Spring Boot拍卖系统实现与实时竞价问题求助
问题1:Postman竞价API 400错误解决
核心原因及修复方案
出现JSON解析异常(无法反序列化为double),通常是以下场景导致:
- 字段类型不匹配:前端传递字符串格式的价格,后端用
double基本类型接收;改用Double包装类可兼容空值校验 - 字段名大小写/命名不一致:比如后端DTO字段是
bidPrice,前端传bidprice或price - 缺少参数校验:未对价格字段做非空校验,导致空值传入触发解析失败
代码示例
1. 竞价请求DTO(带校验)
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; public class BidRequest { @NotNull(message = "竞价价格不能为空") @Positive(message = "竞价价格必须大于0") private Double bidPrice; // 用Double而非double,支持空值校验 @NotNull(message = "商品ID不能为空") private Long productId; // getter/setter }
2. Controller接口(绑定校验)
@PostMapping("/bid") public ResponseEntity<?> placeBid( @RequestBody @Valid BidRequest bidRequest, @AuthenticationPrincipal JwtAuthenticationToken token) { Long userId = (Long) token.getPrincipal(); bidService.placeBid(userId, bidRequest.getProductId(), bidRequest.getBidPrice()); return ResponseEntity.ok("竞价成功"); }
3. Postman请求体示例
{ "bidPrice": 150.8, "productId": 1 }
核心拍卖逻辑实现(可扩展+高效)
1. 实体层扩展
商品实体(新增拍卖状态字段)
@Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Double basePrice; // 起拍价 private Double currentHighestBid; // 当前最高价 private Long highestBidderId; // 当前最高出价用户ID private LocalDateTime auctionStartTime; private LocalDateTime auctionEndTime; private AuctionStatus status; // 拍卖状态枚举 // getter/setter } public enum AuctionStatus { NOT_STARTED, IN_PROGRESS, FINISHED }
竞价记录实体
@Entity public class Bid { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long productId; private Long userId; private Double bidAmount; private LocalDateTime bidTime; // getter/setter }
2. 倒计时重置逻辑(基于Redis实现)
用Redis的键过期事件实现倒计时,分布式环境下更可靠:
- 拍卖启动时,给商品设置Redis键
auction:countdown:{productId},过期时间10秒 - 每次新竞价成功,重置该键的过期时间为10秒
- 监听Redis键过期事件,触发拍卖结束逻辑
Redis配置与监听器
@Configuration public class RedisConfig { @Bean public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); // 订阅键过期事件 container.addMessageListener(new AuctionCountdownListener(), new PatternTopic("__keyevent@0__:expired")); return container; } } @Component public class AuctionCountdownListener implements MessageListener { @Autowired private ProductRepository productRepository; @Override public void onMessage(Message message, byte[] pattern) { String key = new String(message.getBody()); if (key.startsWith("auction:countdown:")) { Long productId = Long.parseLong(key.split(":")[2]); finishAuction(productId); } } private void finishAuction(Long productId) { Product product = productRepository.findById(productId).orElseThrow(); if (product.getStatus() == AuctionStatus.IN_PROGRESS) { product.setStatus(AuctionStatus.FINISHED); product.setAuctionEndTime(LocalDateTime.now()); // 此处可添加通知最高出价用户的逻辑(如MQ异步通知) productRepository.save(product); } } }
竞价业务逻辑(含倒计时重置)
@Service public class BidService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private ProductRepository productRepository; @Autowired private BidRepository bidRepository; public void placeBid(Long userId, Long productId, Double bidAmount) { // 分布式锁防止并发竞价 String lockKey = "auction:lock:" + productId; Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 3, TimeUnit.SECONDS); if (!locked) { throw new RuntimeException("当前竞价繁忙,请稍后重试"); } try { Product product = productRepository.findById(productId).orElseThrow(); // 校验拍卖状态 if (product.getStatus() != AuctionStatus.IN_PROGRESS) { throw new RuntimeException("拍卖未开始或已结束"); } // 校验出价高于当前最高价 if (bidAmount <= (product.getCurrentHighestBid() == null ? product.getBasePrice() : product.getCurrentHighestBid())) { throw new RuntimeException("出价需高于当前最高价"); } // 保存竞价记录 Bid bid = new Bid(); bid.setProductId(productId); bid.setUserId(userId); bid.setBidAmount(bidAmount); bid.setBidTime(LocalDateTime.now()); bidRepository.save(bid); // 更新商品最高价 product.setCurrentHighestBid(bidAmount); product.setHighestBidderId(userId); productRepository.save(product); // 重置倒计时 redisTemplate.expire("auction:countdown:" + productId, 10, TimeUnit.SECONDS); } finally { redisTemplate.delete(lockKey); } } }
3. 推荐依赖库
- Spring Data Redis:处理分布式缓存、锁与键过期事件,实现可靠的倒计时逻辑
- Spring Validation:参数校验,从源头避免400类错误
- Spring AMQP:若需异步通知用户(如拍卖结束通知),可结合RabbitMQ实现解耦
内容的提问来源于stack exchange,提问作者Destello
相关产品推荐
相关产品推荐

