Spring Boot实战:构建高可用盲盒系统,详解加权随机算法与事务控制

📅 发布时间:2026/8/5 15:35:53
Spring Boot实战:构建高可用盲盒系统,详解加权随机算法与事务控制
最近在技术社区看到不少关于“盲盒”类应用开发的讨论很多开发者想尝试这类带有随机性和趣味性的功能但苦于没有一套完整的、可落地的技术方案。无论是电商促销、游戏道具发放还是社区互动活动“随机开箱”都是一个能有效提升用户参与度的经典模式。本文将彻底拆解一个“猛鱼盲盒”系统的后端设计与实现。我们将使用主流的 Spring Boot 框架从需求分析、数据库设计、核心随机算法到完整的 API 开发、事务控制一步步构建一个高可用、防刷的盲盒服务。文章包含所有可运行的代码、详细的配置说明以及线上环境必须考虑的并发、安全等问题。无论你是想学习 Spring Boot 项目实战还是需要为你的应用添加类似功能都能从中获得可直接复用的代码和思路。1. 系统需求与核心概念分析在动手编码之前明确我们要做什么至关重要。一个盲盒系统远不止“随机返回一个物品”那么简单。1.1 业务场景与功能拆解我们的“猛鱼盲盒”系统模拟这样一个场景用户支付一定金额例如3000元可以开启一批例如10个盲盒。每个盲盒开启后随机获得一件“猛鱼”主题的虚拟物品这些物品有不同的稀有度如普通、稀有、史诗、传说并且可能已经“上架”或“未上架”。系统需要记录每次开启记录并确保随机过程的公平性与可追溯性。核心功能清单物品管理后台可配置盲盒池内的物品包括名称、描述、图片、稀有度、概率权重、状态是否可被抽中。盲盒开启用户发起开启请求系统根据配置的概率随机选出物品。批量开启支持一次支付连续开启多个盲盒并返回所有结果。记录与查询完整记录用户的每一次开启操作、消耗、获得的物品用户可查询自己的历史记录。事务与一致性确保扣款或扣除积分与发放物品两个操作是一个原子事务要么都成功要么都失败。防刷与限流防止用户通过脚本高频调用接口进行刷取。1.2 技术栈选型为了高效可靠地实现上述功能我们选择以下技术栈后端框架Spring Boot 2.7.x (成熟稳定生态丰富)数据库MySQL 8.0 (关系型数据库保证事务ACID)持久层框架MyBatis-Plus 3.5.x (简化CRUD提高开发效率)项目管理MavenAPI测试Postman 或 cURL2. 环境准备与项目初始化2.1 基础环境要求请确保你的开发环境已安装JDK 8 或 11 (推荐11本文示例基于JDK 11)Maven 3.6MySQL 8.0 并已启动服务IDE (IntelliJ IDEA 或 Eclipse)2.2 创建Spring Boot项目使用 Spring Initializr ( https://start.spring.io ) 或 IDE 内置工具创建项目。Project: MavenLanguage: JavaSpring Boot: 2.7.18 (选择一个稳定版本)Dependencies: 勾选Spring Web,MyBatis Framework,MySQL Driver生成项目后在pom.xml中手动添加 MyBatis-Plus 依赖它比原生 MyBatis 更强大。!-- pom.xml -- dependencies !-- Spring Boot 基础依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency !-- MyBatis-Plus 核心依赖 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- Lombok 简化实体类代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- Hutool 工具类方便处理随机等 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.20/version /dependency /dependencies2.3 数据库与配置文件创建数据库blind_box_db然后配置application.yml。# src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/blind_box_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root # 请替换为你的数据库用户名 password: your_password # 请替换为你的数据库密码 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL生产环境建议关闭 global-config: db-config: id-type: auto # 主键自增 logic-delete-field: isDeleted # 全局逻辑删除字段名若有 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值 # 自定义配置盲盒单价单位分和每次开启数量 blind-box: price-per-box: 30000 # 300元 30000分避免浮点数计算 open-count-per-request: 103. 数据库设计与核心表结构良好的表结构是系统的基石。我们设计三张核心表。-- 表1盲盒物品表 (blind_box_item) CREATE TABLE blind_box_item ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(100) NOT NULL COMMENT 物品名称如金龙鱼、银龙鱼, description varchar(500) DEFAULT NULL COMMENT 物品描述, image_url varchar(500) DEFAULT NULL COMMENT 物品图片链接, rarity tinyint NOT NULL COMMENT 稀有度1-普通2-稀有3-史诗4-传说, weight int NOT NULL DEFAULT 100 COMMENT 抽取权重概率自身权重/总权重, status tinyint NOT NULL DEFAULT 1 COMMENT 状态0-下架不可抽1-上架可抽, created_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_rarity (rarity), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT盲盒物品池; -- 表2用户开启记录表 (user_open_record) CREATE TABLE user_open_record ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_id bigint NOT NULL COMMENT 用户ID, transaction_id varchar(64) NOT NULL COMMENT 本次开启事务ID用于幂等, open_count int NOT NULL COMMENT 本次开启的盲盒数量, total_cost bigint NOT NULL COMMENT 总花费单位分, open_result json NOT NULL COMMENT 开启结果JSON数组包含获得的物品ID列表, created_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 开启时间, PRIMARY KEY (id), UNIQUE KEY uk_transaction (transaction_id), -- 唯一索引保证幂等 KEY idx_user_time (user_id, created_time) -- 便于按用户查询历史 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户开启记录; -- 表3用户物品库存表 (user_inventory) CREATE TABLE user_inventory ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_id bigint NOT NULL COMMENT 用户ID, item_id bigint NOT NULL COMMENT 物品ID, quantity int NOT NULL DEFAULT 1 COMMENT 拥有数量, obtained_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 获得时间, updated_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_user_item (user_id, item_id), -- 唯一索引一个用户一种物品一条记录 KEY idx_user_id (user_id), KEY idx_item_id (item_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户物品库存;设计要点说明blind_box_item.weight这是实现加权随机算法的关键字段。权重越高被抽中的概率越大。user_open_record.transaction_id使用唯一索引是实现接口幂等性的核心防止用户因网络重试等原因重复扣款。user_open_record.open_result使用 MySQL 的 JSON 类型存储结果方便扩展也便于直接查询分析。user_inventory将用户获得的物品独立建表方便后续展示“我的背包”、交易等功能扩展。4. 核心代码实现4.1 实体类与Mapper首先根据表结构创建对应的 Java 实体类这里使用 Lombok 简化代码。// src/main/java/com/example/blindbox/entity/BlindBoxItem.java package com.example.blindbox.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(blind_box_item) public class BlindBoxItem { TableId(type IdType.AUTO) private Long id; private String name; private String description; private String imageUrl; private Integer rarity; // 1:普通, 2:稀有, 3:史诗, 4:传说 private Integer weight; private Integer status; // 0:下架, 1:上架 private LocalDateTime createdTime; private LocalDateTime updatedTime; }// src/main/java/com/example/blindbox/entity/UserOpenRecord.java package com.example.blindbox.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(user_open_record) public class UserOpenRecord { TableId(type IdType.AUTO) private Long id; private Long userId; private String transactionId; private Integer openCount; private Long totalCost; private String openResult; // JSON字符串 private LocalDateTime createdTime; }// src/main/java/com/example/blindbox/entity/UserInventory.java package com.example.blindbox.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(user_inventory) public class UserInventory { TableId(type IdType.AUTO) private Long id; private Long userId; private Long itemId; private Integer quantity; private LocalDateTime obtainedTime; private LocalDateTime updatedTime; }接着创建对应的 Mapper 接口。MyBatis-Plus 的BaseMapper提供了大部分基础 CRUD 操作。// src/main/java/com/example/blindbox/mapper/BlindBoxItemMapper.java package com.example.blindbox.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.blindbox.entity.BlindBoxItem; import org.apache.ibatis.annotations.Mapper; Mapper public interface BlindBoxItemMapper extends BaseMapperBlindBoxItem { // 可以在此定义自定义的复杂SQL方法 }UserOpenRecordMapper和UserInventoryMapper的创建方式类似此处省略。4.2 加权随机算法服务这是盲盒系统的“心脏”。我们需要一个服务从所有“上架”状态的物品中根据权重随机选取一个。// src/main/java/com/example/blindbox/service/impl/RandomDrawServiceImpl.java package com.example.blindbox.service.impl; import com.example.blindbox.entity.BlindBoxItem; import com.example.blindbox.mapper.BlindBoxItemMapper; import com.example.blindbox.service.RandomDrawService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.ThreadLocalRandom; Slf4j Service RequiredArgsConstructor public class RandomDrawServiceImpl implements RandomDrawService { private final BlindBoxItemMapper blindBoxItemMapper; Override public BlindBoxItem drawOneItem() { // 1. 查询所有上架的可抽取物品 ListBlindBoxItem availableItems blindBoxItemMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapperBlindBoxItem() .eq(BlindBoxItem::getStatus, 1) // 状态为上架 ); if (availableItems.isEmpty()) { log.warn(盲盒池中没有可抽取的物品); throw new RuntimeException(盲盒池为空无法抽取); } // 2. 计算总权重 int totalWeight availableItems.stream().mapToInt(BlindBoxItem::getWeight).sum(); // 3. 生成一个 [0, totalWeight) 之间的随机数 int randomPoint ThreadLocalRandom.current().nextInt(totalWeight); // 4. 根据权重区间选择物品 int currentWeight 0; for (BlindBoxItem item : availableItems) { currentWeight item.getWeight(); if (randomPoint currentWeight) { log.info(随机数[{}]选中物品[{}], 权重[{}], randomPoint, item.getName(), item.getWeight()); return item; } } // 理论上不会走到这里除非权重计算有误 throw new RuntimeException(权重随机算法异常); } }算法解析假设有三个物品 A(权重10)、B(权重30)、C(权重60)。总权重100。随机数范围[0,99]。若随机数落在[0,9]选中A。若随机数落在[10,39]选中B。若随机数落在[40,99]选中C。 这样C被选中的概率就是60/10060%完美符合权重比例。4.3 核心业务服务开启盲盒这个服务需要处理用户请求、调用随机算法、记录日志、更新库存并且所有操作必须在一个事务内完成。// src/main/java/com/example/blindbox/service/impl/BlindBoxServiceImpl.java package com.example.blindbox.service.impl; import cn.hutool.core.util.IdUtil; import com.alibaba.fastjson2.JSON; import com.example.blindbox.config.BlindBoxConfig; import com.example.blindbox.entity.BlindBoxItem; import com.example.blindbox.entity.UserInventory; import com.example.blindbox.entity.UserOpenRecord; import com.example.blindbox.mapper.UserInventoryMapper; import com.example.blindbox.mapper.UserOpenRecordMapper; import com.example.blindbox.service.BlindBoxService; import com.example.blindbox.service.RandomDrawService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; Slf4j Service RequiredArgsConstructor public class BlindBoxServiceImpl implements BlindBoxService { private final RandomDrawService randomDrawService; private final UserOpenRecordMapper userOpenRecordMapper; private final UserInventoryMapper userInventoryMapper; private final BlindBoxConfig blindBoxConfig; Override Transactional(rollbackFor Exception.class) // 声明式事务异常则回滚 public UserOpenRecord openBoxes(Long userId, String clientTransactionId) { // 1. 参数校验与幂等性检查 if (userId null || userId 0) { throw new IllegalArgumentException(用户ID无效); } // 生成最终的事务ID客户端传入ID 时间戳确保唯一性。如果客户端没传则系统生成。 String finalTransactionId (clientTransactionId ! null !clientTransactionId.isEmpty()) ? (clientTransactionId _ System.currentTimeMillis()) : IdUtil.fastSimpleUUID(); // 检查该事务ID是否已处理过防重 Long count userOpenRecordMapper.selectCount( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapperUserOpenRecord() .eq(UserOpenRecord::getTransactionId, finalTransactionId) ); if (count 0) { log.warn(重复的开启请求事务ID: {}, 用户ID: {}, finalTransactionId, userId); // 这里可以直接返回之前的记录实现幂等。或者抛出业务异常。 throw new RuntimeException(请勿重复提交); } // 2. 计算总花费 (从配置读取单价和数量) int openCount blindBoxConfig.getOpenCountPerRequest(); // 10 long unitPrice blindBoxConfig.getPricePerBox(); // 30000 long totalCost openCount * unitPrice; // 300000 分 3000元 // 3. 模拟支付扣款此处应调用真实的支付服务 // boolean paySuccess paymentService.deduct(userId, totalCost); // if (!paySuccess) { throw new RuntimeException(扣款失败); } log.info(模拟用户[{}]扣款[{}]分成功, userId, totalCost); // 4. 核心连续抽取多个盲盒 ListLong drawnItemIds new ArrayList(openCount); ListBlindBoxItem drawnItems new ArrayList(openCount); for (int i 0; i openCount; i) { BlindBoxItem item randomDrawService.drawOneItem(); drawnItemIds.add(item.getId()); drawnItems.add(item); } // 5. 批量更新用户库存使用MyBatis-Plus的saveOrUpdate批量操作 updateUserInventory(userId, drawnItemIds); // 6. 保存开启记录 UserOpenRecord record new UserOpenRecord(); record.setUserId(userId); record.setTransactionId(finalTransactionId); record.setOpenCount(openCount); record.setTotalCost(totalCost); record.setOpenResult(JSON.toJSONString(drawnItemIds)); // 存物品ID列表的JSON record.setCreatedTime(LocalDateTime.now()); userOpenRecordMapper.insert(record); log.info(用户[{}]开启{}个盲盒成功事务ID: {}获得物品: {}, userId, openCount, finalTransactionId, drawnItemIds); // 7. 返回记录可以在这里组装更丰富的返回信息如物品详情列表 // 为方便前端展示我们可以把物品详情也塞进去注意这里修改了实体实际可新建VO // record.setItemDetails(drawnItems); return record; } /** * 更新用户库存存在则数量1不存在则插入 */ private void updateUserInventory(Long userId, ListLong itemIds) { // 按物品ID分组计数 MapLong, Long itemIdCountMap itemIds.stream() .collect(Collectors.groupingBy(id - id, Collectors.counting())); for (Map.EntryLong, Long entry : itemIdCountMap.entrySet()) { Long itemId entry.getKey(); Long addCount entry.getValue(); // 查询用户是否已有该物品 UserInventory inventory userInventoryMapper.selectOne( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapperUserInventory() .eq(UserInventory::getUserId, userId) .eq(UserInventory::getItemId, itemId) ); if (inventory ! null) { // 已有更新数量 inventory.setQuantity(inventory.getQuantity() addCount.intValue()); inventory.setUpdatedTime(LocalDateTime.now()); userInventoryMapper.updateById(inventory); } else { // 没有新增记录 UserInventory newInventory new UserInventory(); newInventory.setUserId(userId); newInventory.setItemId(itemId); newInventory.setQuantity(addCount.intValue()); newInventory.setObtainedTime(LocalDateTime.now()); newInventory.setUpdatedTime(LocalDateTime.now()); userInventoryMapper.insert(newInventory); } } } }4.4 配置类与控制器我们需要一个配置类来读取application.yml中的自定义配置。// src/main/java/com/example/blindbox/config/BlindBoxConfig.java package com.example.blindbox.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; Data Component ConfigurationProperties(prefix blind-box) public class BlindBoxConfig { private Long pricePerBox; // 每个盲盒价格分 private Integer openCountPerRequest; // 每次请求开启数量 }最后创建 RESTful API 控制器。// src/main/java/com/example/blindbox/controller/BlindBoxController.java package com.example.blindbox.controller; import com.example.blindbox.entity.UserOpenRecord; import com.example.blindbox.service.BlindBoxService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/blind-box) RequiredArgsConstructor public class BlindBoxController { private final BlindBoxService blindBoxService; /** * 开启盲盒 * param userId 用户ID (实际应从Token或Session中获取此处简化) * param clientTxId 客户端生成的事务ID用于幂等 (可选) * return 开启记录 */ PostMapping(/open) public ApiResponseUserOpenRecord openBoxes(RequestParam Long userId, RequestParam(required false) String clientTxId) { try { UserOpenRecord record blindBoxService.openBoxes(userId, clientTxId); return ApiResponse.success(record); } catch (IllegalArgumentException e) { return ApiResponse.fail(400, e.getMessage()); } catch (RuntimeException e) { // 捕获业务异常如重复提交、盲盒池为空等 return ApiResponse.fail(500, e.getMessage()); } catch (Exception e) { return ApiResponse.fail(500, 系统开小差了请稍后再试); } } // 可以添加其他接口如查询我的背包、查询历史记录等 } // 简单的统一响应封装类 class ApiResponseT { private int code; private String message; private T data; public static T ApiResponseT success(T data) { ApiResponseT response new ApiResponse(); response.code 200; response.message success; response.data data; return response; } public static T ApiResponseT fail(int code, String message) { ApiResponseT response new ApiResponse(); response.code code; response.message message; return response; } // getters and setters ... }5. 系统测试与验证5.1 准备测试数据向blind_box_item表插入一些测试物品。INSERT INTO blind_box_item (name, description, rarity, weight, status) VALUES (小丑鱼, 可爱的普通小鱼, 1, 500, 1), (蓝魔鱼, 泛着蓝光的稀有鱼种, 2, 300, 1), (七彩神仙鱼, 色彩斑斓的史诗级观赏鱼, 3, 150, 1), (金龙鱼, 传说中的鱼王价值连城, 4, 50, 1), (鲤鱼, 非常常见的鱼类, 1, 400, 1); -- 总权重50030015050400 1400 -- 金龙鱼概率50/1400 ≈ 3.57%5.2 启动与接口调用启动 Spring Boot 应用。使用 Postman 或 curl 调用接口。# 使用 curl 测试 curl -X POST \ http://localhost:8080/api/blind-box/open?userId10001clientTxIdtx_123456 \ -H Content-Type: application/json预期成功响应{ code: 200, message: success, data: { id: 1, userId: 10001, transactionId: tx_123456_1646389200000, openCount: 10, totalCost: 300000, openResult: [3, 1, 5, 2, 1, 4, 1, 2, 5, 3], createdTime: 2023-10-27T10:20:00 } }同时检查数据库user_open_record表会新增一条记录user_inventory表会更新用户 10001 的物品库存。5.3 测试幂等性用相同的userId和clientTxId再调用一次接口应该收到“请勿重复提交”的错误提示并且数据库不会新增记录和库存保证了用户不会因重复请求而被多次扣款。6. 生产环境进阶考量与优化一个玩具级的demo和线上可用的系统之间隔着许多工程化细节。6.1 性能优化缓存物品池blind_box_item表数据变动不频繁可以缓存在 Redis 中避免每次抽奖都查询数据库。注意缓存更新策略。批量库存更新优化当前updateUserInventory方法在循环中查询和更新可以优化为一次性查询出用户已有的所有相关物品在内存中计算后使用 MyBatis-Plus 的saveOrUpdateBatch进行批量操作。异步记录开启记录 (UserOpenRecord) 的写入可以改为异步放入消息队列如 RabbitMQ/Kafka后慢慢消费提升接口响应速度。但核心的业务逻辑扣款、抽奖、更新库存必须同步且在事务内。6.2 安全与防刷用户身份认证示例中直接传userId极不安全。必须集成 Spring Security 或 JWT从 Token 中解析用户身份。频率限制使用 Redis 实现滑动窗口限流例如限制每个用户每分钟只能开启 N 次。风控策略监控异常开启模式如同一个IP在极短时间内用不同账号大量开启应触发风控报警或临时封禁。概率审计定期对抽奖日志进行统计分析确保实际产出分布与配置的概率权重相符防止程序bug导致概率失真。6.3 事务与数据一致性深入分布式事务如果“扣款”是一个独立的微服务那么“扣款”和“发放物品”就构成了一个分布式事务场景。需要考虑使用 Seata 等分布式事务框架或最终一致性方案如扣款后发MQ盲盒服务消费MQ。补偿机制在分布式场景下如果发放物品失败需要有机制触发扣款回退退款。6.4 监控与运维日志标准化使用 SLF4J Logback规范日志格式记录关键业务事件如用户开启、获得稀有物品。配置中心将盲盒单价、每次开启数量、物品概率权重等配置项迁移到 Apollo 或 Nacos支持动态刷新无需重启服务。数据库监控监控user_open_record和user_inventory表的增长情况提前做好分库分表规划。7. 常见问题排查清单在实际开发和部署中你可能会遇到以下问题问题现象可能原因排查步骤与解决方案启动报错Failed to configure a DataSource数据库连接配置错误或驱动不匹配1. 检查application.yml中的url,username,password。2. 确认 MySQL 服务是否启动。3. 检查pom.xml中 MySQL 驱动版本是否与数据库版本兼容。抽奖时一直报“盲盒池为空”1.blind_box_item表无数据。2. 所有物品status都为 0下架。1. 连接数据库执行SELECT * FROM blind_box_item WHERE status 1;查看是否有可抽物品。2. 检查 RandomDrawService 中查询条件是否正确。接口返回成功但库存没增加1. 事务未生效。2. 更新库存的代码逻辑有误。3. 异常被捕获未抛出导致事务未回滚但业务失败。1. 确认 Service 方法上有Transactional注解。2. 在updateUserInventory方法内打日志查看 SQL 是否执行。3. 检查 Controller 的catch (Exception e)是否捕获了业务异常并处理应确保业务异常能触发回滚。高并发下库存数量不对并发更新导致“超卖”或“少卖”。1.悲观锁在查询用户库存时使用SELECT ... FOR UPDATE。2.乐观锁在user_inventory表增加version字段更新时带条件WHERE id? AND version?。3. 将库存更新操作放入队列串行处理。概率感觉不对传说鱼一直抽不到1. 权重配置不合理传说物品权重过低。2. 随机数生成器有偏。3. 程序逻辑错误。1. 写一个测试程序循环抽取10万次统计各物品出现频率与理论概率对比。2. 检查ThreadLocalRandom.current().nextInt(totalWeight)的边界是否正确应传入总权重。3. 打印每次抽奖的随机数和选中的物品进行人工复核。通过以上从设计到实现再到优化和排错的完整流程我们构建了一个功能完备、考虑周到的“猛鱼盲盒”后端系统。这个项目不仅涵盖了 Spring Boot MyBatis-Plus 的常规开发更深入到了事务、幂等、算法、防刷等后端核心领域。你可以在此基础上继续扩展前端界面、管理后台、物品交易市场等功能形成一个完整的虚拟盲盒项目。