开发者

基于SpringBoot实现抽奖活动的四种策略

开发者 https://www.devze.com 2025-06-28 14:04 出处:网络 作者: 风象南
目录一、基于内存的简单抽奖策略1.1 基本原理1.2 实现方式1.3 优缺点分析1.4 适用场景二、基于数据库的抽奖策略2.1 基本原理2.2 实现方式2.3 优缺点分析2.4 适用场景三、基于Redis的高性能抽奖策略3.1 基本原理3.2 实
目录
  • 一、基于内存的简单抽奖策略
    • 1.1 基本原理
    • 1.2 实现方式
    • 1.3 优缺点分析
    • 1.4 适用场景
  • 二、基于数据库的抽奖策略
    • 2.1 基本原理
    • 2.2 实现方式
    • 2.3 优缺点分析
    • 2.4 适用场景
  • 三、基于Redis的高性能抽奖策略
    • 3.1 基本原理
    • 3.2 实现方式
    • 3.3 优缺点分析
    • 3.4 适用场景
  • 四、基于权重概率的抽奖策略
    • 4.1 基本原理
    • 4.2 实现方式
    • 4.3 优缺点分析
    • 4.4 适用场景
  • 五、方案对比
    • 6.1 性能对比
    • 6.2 功能对比
  • 六、结语

    一、基于内存的简单抽奖策略

    1.1 基本原理

    最简单的抽奖策略是将所有奖品信息加载到内存中,通过随机数算法从奖品池中选取一个奖品。

    这种方式实现简单,适合奖品种类少、规则简单的小型抽奖活动。

    1.2 实现方式

    首先定义奖品实体:

    @Data
    public class Prize {
        private Long id;
        private String name;
        private String description;
        private Integer probability; // 中奖概率,1-10000之间的数字,表示万分之几
        private Integer stock;       // 库存
        private Boolean available;   // 是否可用
    }
    

    然后实现抽奖服务:

    @Service
    public class SimpleDrawService {
        
        private final List<Prize> prizePool = new ArrayList<>();
        private final Random random = new Random();
        
        // 初始化奖品池
        @PostConstruct
        public void init() {
            // 奖品1: 一等奖,概率0.01%,库存10
            Prize firstPrize = new Prize();
            firstPrize.setId(1L);
            firstPrize.setName("一等奖");
            firstPrize.setDescription("iPhone 14 Pro");
            firstPrize.setProbability(1); // 万分之1
            firstPrize.setStock(10);
            firstPrize.setAvailable(true);
            
            // 奖品2: 二等奖,概率0.1%,库存50
            Prize secondPrize = new Prize();
            secondPrize.setId(2L);
            secondPrize.setName("二等奖");
            secondPrize.setDescription("AirPods Pro");
            secondPrize.setProbability(10); // 万分之10
            secondPrize.setStock(50);
            secondPrize.setAvailable(true);
            
            // 奖品3: 三等奖,概率1%,库存500
            Prize thirdPrize = new Prize();
            thirdPrize.setId(3L);
            thirdPrize.setName("三等奖");
            thirdPrize.setDescription("100元优惠券");
            thirdPrize.setProbability(100); // 万分之100
            thirdPrize.setStock(500);
            thirdPrize.setAvailable(true);
            
            // 奖品4: 谢谢参与,概率98.89%,无限库存
            Prize noPrize = new Prize();
            noPrize.setId(4L);
            noPrize.setName("谢谢参与");
            noPrize.setDescription("再接再厉");
            noPrize.setProbability(9889); // 万分之9889
            noPrize.setStock(Integer.MAX_VALUE);
            noPrize.setAvailable(true);
            
            prizePool.add(firstPrize);
            prizePool.add(secondPrize);
            prizePool.add(thirdPrize);
            prizePool.add(noPrize);
        }
        
        // 抽奖方法
        public synchronized Prize draw() {
            // 生成一个1-10000之间的随机数
            int randomNum = random.nextInt(10000) + 1;
            
            int probabilitySum = 0;
            for (Prize prize : prizePool) {
                if (!prize.getAvailable() || prize.getStock() <= 0) {
                    continue; // 跳过不可用或无库存的奖品
                }
                
                probabilitySum += prize.getProbability();
                if (randomNum <= probabilitySum) {
                    // 减少库存
                    prize.setStock(prize.getStock() - 1);
                    
                    // 如果库存为0,设置为不可用
                    if (prize.getStock() <= 0) {
                        prize.setAvailable(false);
                    }
                    
                    return prize;
                }
            }
            
            // 如果所有奖品都不可用,返回默认奖品
            return getDefaultPrize();
        }
        
        private Prize getDefaultPrize() {
            for (Prize prize : prizePool) {
                if (prize.getName().equals("谢谢参与")) {
                    return prize;
                }
            }
            
            // 创建一个默认奖品
            Prize defaultPrize = new Prize();
            defaultPrize.setId(999L);
            defaultPrize.setName("谢谢参与");
            defaultPrize.setDescription("再接再厉");
            return defaultPrize;
        }
    }
    

    控制器实现:

    @RestController
    @RequestMapping("/api/draw")
    public class DrawController {
        
        @Autowired
        private SimpleDrawService drawService;
        
        @GetMapping("/simple")
        public Prize simpleDraw() {
            return drawService.draw();
        }
    }
    

    1.3 优缺点分析

    优点:

    • 实现简单,开发成本低
    • 无需数据库支持,启动即可使用

    缺点:

    • 不适合大规模并发场景
    • 服务重启后数据丢失,无法保证奖品总量控制
    • 难以实现用户抽奖次数限制和作弊防护

    1.4 适用场景

    • 小型活动或测试环境
    • 奖品总量不敏感的场景
    • 单机部署的简单应用
    • 对抽奖公平性要求不高的场景

    二、基于数据库的抽奖策略

    2.1 基本原理

    将奖品信息、抽奖记录等数据存储在数据库中,通过数据库事务来保证奖品库存的准确性和抽奖记录的完整性。

    这种方式适合需要持久化数据并且对奖品库存有严格管理要求的抽奖活动。

    2.2 实现方式

    数据库表设计:

    -- 奖品表
    CREATE TABLE prize (
        id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(100) NOT NULL,
        description VARCHAR(255),
        probability INT NOT NULL COMMENT '中奖概率,1-10000之间的数字,表示万分之几',
        stock INT NOT NULL COMMENT '库存',
        available BOOLEAN DEFAULT TRUE COMMENT '是否可用'
    );
    
    -- 抽奖记录表
    CREATE TABLE draw_record (
        id BIGINT PRIMARY KEY AUTO_INCREMENT,
        user_id BIGINT NOT NULL COMMENT '用户ID',
        prize_id BIGINT COMMENT '奖品ID',
        draw_time DATETIME NOT NULL COMMENT '抽奖时间',
        ip VARCHAR(50) COMMENT '用户IP地址',
        INDEX idx_user_id (user_id)
    );
    
    -- 抽奖活动表
    CREATE TABLE draw_activity (
        id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(100) NOT NULL COMMENT '活动名称',
        start_time DATETIME NOT NULL COMMENT '开始时间',
        end_time DATETIME NOT NULL COMMENT '结束时间',
        daily_limit INT DEFAULT 1 COMMENT '每人每日抽奖次数限制',
        total_limit INT DEFAULT 10 COMMENT '每人总抽奖次数限制',
        active BOOLEAN DEFAULT TRUE COMMENT '是否激活'
    );
    

    实体类:

    @Data
    @Entity
    @Table(name = "prize")
    public class Prize {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        private String name;
        
        private String description;
        
        private Integer probability;
        
        private Integer stock;
        
        private Boolean available;
    }
    
    @Data
    @Entity
    @Table(name = "draw_record")
    public class DrawR编程客栈ecord {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        @Column(name = "user_id")
        private Long userId;
        
        @Column(name = "prize_id")
        private Long prizeId;
        
        @Column(name = "draw_time")
        private LocalDateTime drawTime;
        
        private String ip;
    }
    
    @Data
    @Entity
    @Table(name = "draw_activity")
    public class DrawActivity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        private String name;
        
        @Column(name = "start_time")
        private LocalDateTime startTime;
        
        @Column(name = "end_time")
        private LocalDateTime endTime;
        
        @Column(name = "daily_limit")
        private Integer dailyLimit;
        
        @Column(name = "total_limit")
        private Integer totalLimit;
        
        private Boolean active;
    }
    

    Repository 接口:

    public interface PrizeRepository extends JpaRepository<Prize, Long> {
        List<Prize> findByAvailableTrueAndStockGreaterThan(int stock);
    }
    
    public interface DrawRecordRepository extends JpaRepository<DrawRecord, Long> {
        long countByUserIdAndDrawTimeBetween(Long userId, LocalDateTime start, LocalDateTime end);
        
        long countByUserId(Long userId);
    }
    
    public interface DrawActivityRepository extends JpaRepository<DrawActivity, Long> {
        Optional<DrawActivity> findByActiveTrue();
    }
    

    服务实现:

    @Service
    @Transactional
    public class DatabaseDrawService {
        
        @Autowired
        private PrizeRepository prizeRepository;
        
        @Autowired
        private DrawRecordRepository drawRecordRepository;
        
        @Autowired
        private DrawActivityRepository drawActivityRepository;
        
        private final Random random = new Random();
        
        public Prize draw(Long userId, String ip) {
            // 检查活动是否有效
            DrawActivity activity = drawActivityRepository.findByActiveTrue()
                    .orElseThrow(() -> new RuntimeException("No active draw activity"));
            
            LocalDateTime now = LocalDateTime.now();
            if (now.isBefore(activity.getStartTime()) || now.isAfter(activity.getEndTime())) {
                throw new RuntimeException("Draw activity is not in progress");
            }
            
            // 检查用户抽奖次数限制
            checkDrawLimits(userId, activity);
            
            // 获取所有可用奖品
            List<Prize> availablePrizes = prizeRepository.findByAvailableTrueAndStockGreaterThan(0);
            if (availablePrizes.isEmpty()) {
                throw new RuntimeException("No available prizes");
            }
            
            // 计算总概率
            int totalProbability = availablePrizes.stream()
                    .mapToInt(Prize::getProbability)
                    .sum();
            
            // 生成随机数
            int randomNum = random.nextInt(totalProbability) + 1;
            
            // 根据概率选择奖品
            int probabilitySum = 0;
            Prize selectedPrize = null;
            
            for (Prize prize : availablePrizes) {
                probabilitySum += prize.getProbability();
                if (randomNum <= probabilitySum) {
                    selectedPrize = prize;
                    break;
                }
            }
            
            if (selectedPrize == null) {
                throw new RuntimeException("Failed to select a prize");
            }
            
            // 减少库存
            selectedPrize.setStock(selectedPrize.getStock() - 1);
            if (selectedPrize.getStock() <= 0) {
                selectedPrize.setAvailable(false);
            }
            prizeRepository.save(selectedPrize);
            
            // 记录抽奖
            DrawRecord record = new DrawRecord();
            record.setUserId(userId);
            record.setPrizeId(selectedPrize.getId());
            record.setDrawTime(now);
            record.setIp(ip);
            drawRecordRepository.save(record);
            
            return selectedPrize;
        }
        
        private void checkDrawLimits(Long userId, DrawActivity activity) {
            // 检查每日抽奖次数限制
            LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
            LocalDateTime endOfDay = LocalDate.now().plusDays(1).atStartOfDay().minusNanos(1);
            
            long dailyDraws = drawRecordRepository.countByUserIdAndDrawTimeBetween(userId, startOfDay, endOfDay);
            if (dailyDraws >= activity.getDailyLimit()) {
                throw new RuntimeException("Daily draw limit exceeded");
            }
            
            // 检查总抽奖次数限制
            long totalDraws = drawRecordRepository.countByUserId(userId);
            if (totalDraws >= activity.getTotalLimit()) {
                throw new RuntimeException("Total draw limit exceeded");
            }
        }
    }
    

    控制器实现:

    @RestController
    @RequestMapping("/api/draw")
    public class DatabaseDrawController {
        
        @Autowired
        private DatabaseDrawService databaseDrawService;
        
        @GetMapping("/database")
        public Prize databaseDraw(@RequestParam Long userId, HttpServletRequest request) {
            String ip = request.getRemoteAddr();
            return databaseDrawService.draw(userId, ip);
        }
    }
    

    2.3 优缺点分析

    优点:

    • 数据持久化,服务重启不丢失
    • 可靠的库存管理和抽奖记录
    • 支持用户抽奖次数限制和活动时间控制
    • 易于扩展其他业务需求

    缺点:

    • 数据库操作带来的性能开销
    • 高并发场景下可能出现数据库瓶颈
    • 实现相对复杂,开发成本较高

    2.4 适用场景

    • 中小型抽奖活动
    • 需要精确控制奖品库存的场景
    • 需要完整抽奖记录和数据分析的场景

    三、基于Redis的高性能抽奖策略

    3.1 基本原理

    利用Redis的高性能和原子操作特性来实现抽奖系统,将奖品信息和库存存储在Redis中,通过Lua脚本实现原子抽奖操作。这种方式适合高并发抽奖场景,能够提供极高的性能和可靠的数据一致性。

    3.2 实现方式

    首先配置Redis:

    @Configuration
    public class RedisConfig {
        
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);
            
            Jackson2jsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            
            // key采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            // hash的key也采用String的序列化方式
            template.setHashKeySerializer(stringRedisSerializer);
            // value序列化方式采用jackson
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // hash的value序列化方式采用jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            
            template.afterPropertiesSet();
           编程 return template;
        }
    }
    

    抽奖服务实现:

    @Service
    public class RedisDrawService {
        
        private static final String PRIZE_HASH_KEY = "draw:prizes";
        private static final String DAILY_DRAW_COUNT_KEY = "draw:daily:";
        private static final String TOTAL_DRAW_COUNT_KEY = "draw:total:";
        private static final String DRAW_RECORD_KEY = "draw:records:";
        
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        
        @PostConstruct
        public void init() {
            // 初始化奖品数据
            if (!redisTemplate.hasKey(PRIZE_HASH_KEY)) {
                Map<String, Prize> prizes = new HashMap<>();
                
                Prize firstPrize = new Prize();
                firstPrize.setId(1L);
                firstPrize.setName("一等奖");
                firstPrize.setDescription("iPhone 14 Pro");
                firstPrize.setProbability(1); // 万分之1
                firstPrize.setStock(10);
                firstPrize.setAvailable(true);
                prizes.put("1", firstPrize);
                
                Prize secondPrize = new Prize();
                secondPrize.setId(2L);
                secondPrize.setName("二等奖");
                secondPrize.setDescription("AirPods Pro");
                secondPrize.setProbability(10); // 万分之10
                secondPrize.setStock(50);
                secondPrize.setAvailable(true);
                prizes.put("2", secondPrize);
                
                Prize thirdPrize = new Prize();
                thirdPrize.setId(3L);
                thirdPrize.setName("三等奖");
                thirdPrize.setDescription("100元优惠券");
                thirdPrize.setProbability(100); // 万分之100
                thirdPrize.setStock(500);
                thirdPrize.setAvailable(true);
                prizes.put("3", thirdPrize);
                
                Prize noPrize = new Prize();
                noPrize.setId(4L);
                noPrize.setName("谢谢参与");
                noPrize.setDescription("再接再厉");
                noPrize.setProbability(9889); // 万分之9889
                noPrize.setStock(Integer.MAX_VALUE);
                noPrize.setAvailable(true);
                prizes.put("4", noPrize);
                
                // 将奖品信息存储到Redis
                redisTandroidemplate.opsForHash().putAll(PRIZE_HASH_KEY, prizes);
          http://www.devze.com  }
        }
        
        public Prize draw(Long userId) {
            // 检查用户抽奖限制
            checkDrawLimits(userId);
            
            // 获取所有可用奖品
            Map<Object, Object> prizeMap = redisTemplate.opsForHash().entries(PRIZE_HASH_KEY);
            List<Prize> availablePrizes = new ArrayList<>();
            
            for (Object obj : prizeMap.values()) {
                Prize prize = (Prize) obj;
                if (prize.getAvailable() && prize.getStock() > 0) {
                    availablePrizes.add(prize);
                }
            }
            
            if (availablePrizes.isEmpty()) {
                throw new RuntimeException("No available prizes");
            }
            
            // 使用Lua脚本进行原子抽奖操作
            String script = "local prizes = redis.call('HGETALL', KEYS[1]) " +
                    "local random = math.random(1, 10000) " +
                    "local sum = 0 " +
                    "local selected = nil " +
                    "for id, prize in pairs(prizes) do " +
                    "  if prize.available and prize.stock > 0 then " +
                    "    sum = sum + prize.probability " +
                    "    if random <= sum then " +
                    "      selected = prize " +
                    "      prize.stock = prize.stock - 1 " +
                    "      if prize.stock <= 0 then " +
                    "        prize.available = false " +
                    "      end " +
                    "      redis.call('HSET', KEYS[1], id, prize) " +
                    "      break " +
                    "    end " +
                    "  end " +
                    "end " +
                    "return selected";
            
            // 由于Lua脚本在Redis中执行复杂对象有限制,我们这里简化处理,使用Java代码模拟
            // 实际生产环境建议使用更细粒度的Redis数据结构和脚本
            
            // 模拟抽奖逻辑
            Prize selectedPrize = drawprizeFromPool(availablePrizes);
            
            // 减少库存并更新Redimjvhjs
            selectedPrize.setStock(selectedPrize.getStock() - 1);
            if (selectedPrize.getStock() <= 0) {
                selectedPrize.setAvailable(false);
            }
            redisTemplate.opsForHash().put(PRIZE_HASH_KEY, selectedPrize.getId().toString(), selectedPrize);
            
            // 记录抽奖
            incrementUserDrawCount(userId);
            recordUserDraw(userId, selectedPrize);
            
            return selectedPrize;
        }
        
        private Prize drawPrizeFromPool(List<Prize> prizes) {
            int totalProbability = prizes.stream()
                    .mapToInt(Prize::getProbability)
                    .sum();
            
            int randomNum = new Random().nextInt(totalProbability) + 1;
            
            int probabilitySum = 0;
            for (Prize prize : prizes) {
                probabilitySum += prize.getProbability();
                if (randomNum <= probabilitySum) {
                    return prize;
                }
            }
            
            // 默认返回最后一个奖品(通常是"谢谢参与")
            return prizes.get(prizes.size() - 1);
        }
        
        private void checkDrawLimits(Long userId) {
            // 检查每日抽奖次数
            String dailyKey = DAILY_DRAW_COUNT_KEY + userId + ":" + LocalDate.now();
            Integer dailyCount = (Integer) redisTemplate.opsForValue().get(dailyKey);
            
            if (dailyCount != null && dailyCount >= 3) { // 假设每日限制3次
                throw new RuntimeException("Daily draw limit exceeded");
            }
            
            // 检查总抽奖次数
            String totalKey = TOTAL_DRAW_COUNT_KEY + userId;
            Integer totalCount = (Integer) redisTemplate.opsForValue().get(totalKey);
            
            if (totalCount != null && totalCount >= 10) { // 假设总限制10次
                throw new RuntimeException("Total draw limit exceeded");
            }
        }
        
        private void incrementUserDrawCount(Long userId) {
            // 增加每日抽奖次数
            String dailyKey = DAILY_DRAW_COUNT_KEY + userId + ":" + LocalDate.now();
            redisTemplate.opsForValue().increment(dailyKey, 1);
            // 设置过期时间(第二天凌晨过期)
            long secondsUntilTomorrow = ChronoUnit.SECONDS.between(
                    LocalDateTime.now(), 
                    LocalDate.now().plusDays(1).atStartOfDay());
            redisTemplate.expire(dailyKey, secondsUntilTomorrow, TimeUnit.SECONDS);
            
            // 增加总抽奖次数
            String totalKey = TOTAL_DRAW_COUNT_KEY + userId;
            redisTemplate.opsForValue().increment(totalKey, 1);
        }
        
        private void recordUserDraw(Long userId, Prize prize) {
            String recordKey = DRAW_RECORD_KEY + userId;
            Map<String, Object> record = new HashMap<>();
            record.put("userId", userId);
            record.put("prizeId", prize.getId());
            record.put("prizeName", prize.getName());
            record.put("drawTime", LocalDateTime.now().toString());
            
            redisTemplate.opsForList().leftPush(recordKey, record);
        }
    }
    

    控制器实现:

    @RestController
    @RequestMapping("/api/draw")
    public class RedisDrawController {
        
        @Autowired
        private RedisDrawService redisDrawService;
        
        @GetMapping("/redis")
        public Prize redisDraw(@RequestParam Long userId) {
            return redisDrawService.draw(userId);
        }
    }
    

    3.3 优缺点分析

    优点:

    • 极高的性能,支持高并发场景
    • 原子操作保证数据一致性
    • 内存操作,响应速度快
    • Redis持久化保证数据不丢失

    缺点:

    • 实现复杂度较高,尤其是Lua脚本部分
    • 依赖Redis
    • 可能需要定期同步数据到数据库

    3.4 适用场景

    • 高并发抽奖活动
    • 对响应速度要求较高的场景
    • 大型营销活动
    • 需要实时库存控制的抽奖系统

    四、基于权重概率的抽奖策略

    4.1 基本原理

    基于权重概率的抽奖策略是在普通抽奖基础上增加了更复杂的概率计算逻辑,可以根据用户特征、活动规则动态调整奖品中奖概率。

    例如,可以根据用户等级、消费金额、活动参与度等因素调整抽奖权重,实现精细化控制。

    4.2 实现方式

    首先定义动态权重计算接口:

    public interface Weightcalculator {
        // 根据用户信息计算权重调整因子
        double calculateWeightFactor(Long userId);
    }
    
    // VIP用户权重计算器
    @Component
    public class VipWeightCalculator implements WeightCalculator {
        
        @Autowired
        private UserService userService;
        
        @Override
        public double calculateWeightFactor(Long userId) {
            User user = userService.getUserById(userId);
            
            // 根据用户VIP等级调整权重
            switch (user.getVipLevel()) {
                case 0: return 1.0;  // 普通用户,不调整
                case 1: return 1.2;  // VIP1,提高20%中奖率
                case 2: return 1.5;  // VIP2,提高50%中奖率
                case 3: return 2.0;  // VIP3,提高100%中奖率
                default: return 1.0;
            }
        }
    }
    
    // 新用户权重计算器
    @Component
    public class NewUserWeightCalculator implements WeightCalculator {
        
        @Autowired
        private UserService userService;
        
        @Override
        public double calculateWeightFactor(Long userId) {
            User user = userService.getUserById(userId);
            
            // 注册时间少于7天的新用户提高中奖率
            if (ChronoUnit.DAYS.between(user.getRegistrationDate(), LocalDate.now()) <= 7) {
                return 1.5; // 提高50%中奖率
            }
            
            return 1.0;
        }
    }
    
    // 活跃度权重计算器
    @Component
    public class ActivityWeightCalculator implements WeightCalculator {
        
        @Autowired
        private UserActivityService userActivityService;
        
        @Override
        public double calculateWeightFactor(Long userId) {
            int activityScore = userActivityService.getActivityScore(userId);
            
            // 根据活跃度调整权重
            if (activityScore >= 100) {
                return 1.3; // 提高30%中奖率
            } else if (activityScore >= 50) {
                return 1.1; // 提高10%中奖率
            }
            
            return 1.0;
        }
    }
    

    然后实现基于权重的抽奖服务:

    @Service
    public class WeightedDrawService {
        
        @Autowired
        private PrizeRepository prizeRepository;
        
        @Autowired
        private DrawRecordRepository drawRecordRepository;
        
        @Autowired
        private List<WeightCalculator> weightCalculators;
        
        private final Random random = new Random();
        
        public Prize draw(Long userId) {
            // 获取所有可用奖品
            List<Prize> availablePrizes = prizeRepository.findByAvailableTrueAndStockGreaterThan(0);
            if (availablePrizes.isEmpty()) {
                throw new RuntimeException("No available prizes");
            }
            
            // 计算用户的总权重因子
            double weightFactor = calculateTotalWeightFactor(userId);
            
            // 创建带权重的奖品列表
            List<WeightedPrize> weightedPrizes = createWeightedPrizeList(availablePrizes, weightFactor);
            
            // 根据权重选择奖品
            Prize selectedPrize = selectPrizeByWeight(weightedPrizes);
            
            // 减少库存
            selectedPrize.setStock(selectedPrize.getStock() - 1);
            if (selectedPrize.getStock() <= 0) {
                selectedPrize.setAvailable(false);
            }
            prizeRepository.save(selectedPrize);
            
            // 记录抽奖
            recordDraw(userId, selectedPrize);
            
            return selectedPrize;
        }
        
        private double calculateTotalWeightFactor(Long userId) {
            // 从所有权重计算器获取权重并相乘
            return weightCalculators.stream()
                    .mapToDouble(calculator -> calculator.calculateWeightFactor(userId))
                    .reduce(1.0, (a, b) -> a * b);
        }
        
        private List<WeightedPrize> createWeightedPrizeList(List<Prize> prizes, double weightFactor) {
            List<WeightedPrize> weightedPrizes = new ArrayList<>();
            
            for (Prize prize : prizes) {
                WeightedPrize weightedPrize = new WeightedPrize();
                weightedPrize.setPrize(prize);
                
                // 调整中奖概率
                if (prize.getName().equals("谢谢参与")) {
                    // 对于"谢谢参与",权重因子反向作用(权重越高,越不容易"谢谢参与")
                    weightedPrize.setAdjustedProbability((int) (prize.getProbability() / weightFactor));
                } else {
                    // 对于实际奖品,权重因子正向作用(权重越高,越容易中奖)
                    weightedPrize.setAdjustedProbability((int) (prize.getProbability() * weightFactor));
                }
                
                weightedPrizes.add(weightedPrize);
            }
            
            return weightedPrizes;
        }
        
        private Prize selectPrizeByWeight(List<WeightedPrize> weightedPrizes) {
            // 计算总概率
            int totalProbability = weightedPrizes.stream()
                    .mapToInt(WeightedPrize::getAdjustedProbability)
                    .sum();
            
            // 生成随机数
            int randomNum = random.nextInt(totalProbability) + 1;
            
            // 根据概率选择奖品
            int probabilitySum = 0;
            for (WeightedPrize weightedPrize : weightedPrizes) {
                probabilitySum += weightedPrize.getAdjustedProbability();
                if (randomNum <= probabilitySum) {
                    return weightedPrize.getPrize();
                }
            }
            
            // 默认返回最后一个奖品(通常是"谢谢参与")
            return weightedPrizes.get(weightedPrizes.size() - 1).getPrize();
        }
        
        private void recordDraw(Long userId, Prize prize) {
            DrawRecord record = new DrawRecord();
            record.setUserId(userId);
            record.setPrizeId(prize.getId());
            record.setDrawTime(LocalDateTime.now());
            drawRecordRepository.save(record);
        }
        
        // 带权重的奖品类
        @Data
        private static class WeightedPrize {
            private Prize prize;
            private int adjustedProbability;
        }
    }
    

    控制器实现:

    @RestController
    @RequestMapping("/api/draw")
    public class WeightedDrawController {
        
        @Autowired
        private WeightedDrawService weightedDrawService;
        
        @GetMapping("/weighted")
        public Prize weightedDraw(@RequestParam Long userId) {
            return weightedDrawService.draw(userId);
        }
    }
    

    4.3 优缺点分析

    优点:

    • 支持根据用户特征和业务规则动态调整中奖概率
    • 可以实现精细化营销和用户激励
    • 提高高价值用户的体验和留存
    • 灵活的权重计算机制,易于扩展

    缺点:

    • 逻辑复杂,实现和维护成本高
    • 可能影响抽奖公平性,需要谨慎处理
    • 需要收集和分析更多用户数据

    4.4 适用场景

    • 需要精细化运营的大型营销活动
    • 用户分层明显的应用
    • 希望提高特定用户群体体验的场景
    • 有用户激励和留存需求的平台

    五、方案对比

    6.1 性能对比

    抽奖策略响应速度并发支持资源消耗扩展性
    内存抽奖极快
    数据库抽奖中等中等中等
    Redis抽奖中等
    权重抽奖中等中等

    6.2 功能对比

    抽奖策略奖品管理抽奖记录用户限制防作弊定制性
    内存抽奖基础
    数据库抽奖完善完善支持基础中等
    Redis抽奖完善完善支持中等
    权重抽奖完善完善支持极高

    六、结语

    在实际项目中,我们需要根据业务需求、用户规模、性能要求等因素,选择合适的抽奖策略或组合多种策略,以构建高效、可靠、安全的抽奖系统。

    无论选择哪种抽奖策略,都需要关注系统的公平性、性能、可靠性和安全性,不断优化和改进。

    以上就是基于SpringBoot实现抽奖活动的四种策略的详细内容,更多关于SpringBoot抽奖活动的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号