SpringBoot的4种抽奖活动实现策略

抽奖活动是产品运营中常见的用户激励和互动手段,通过随机性和奖励刺激用户参与度,提升用户活跃度和留存率。

在技术实现上,抽奖系统涉及到随机算法、奖品分配、防作弊机制等多方面内容。

本文将介绍基于SpringBoot实现抽奖活动的5种策略。

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

1.1 基本原理

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

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

1.2 实现方式

首先定义奖品实体:

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

然后实现抽奖服务:

scss 复制代码
@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;
    }
}

控制器实现:

less 复制代码
@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 实现方式

数据库表设计:

sql 复制代码
-- 奖品表
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 '是否激活'
);

实体类:

less 复制代码
@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 DrawRecord {
    @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 接口:

csharp 复制代码
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();
}

服务实现:

scss 复制代码
@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");
        }
    }
}

控制器实现:

less 复制代码
@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:

arduino 复制代码
@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;
    }
}

抽奖服务实现:

java 复制代码
@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
            redisTemplate.opsForHash().putAll(PRIZE_HASH_KEY, prizes);
        }
    }
    
    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);
        
        // 减少库存并更新Redis
        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);
    }
}

控制器实现:

less 复制代码
@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 实现方式

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

kotlin 复制代码
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;
    }
}

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

scss 复制代码
@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;
    }
}

控制器实现:

less 复制代码
@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抽奖 完善 完善 支持 中等
权重抽奖 完善 完善 支持 极高

六、结语

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

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

相关推荐
N_NAN_N19 分钟前
[蓝桥杯 2024 国 Python B] 设计
java·数据结构·算法·并查集
他日若遂凌云志26 分钟前
Lua 模块系统的前世今生:从 module () 到 local _M 的迭代
后端
David爱编程27 分钟前
Docker 安全全揭秘:防逃逸、防漏洞、防越权,一篇学会容器防御!
后端·docker·容器
小码编匠31 分钟前
WinForm 工业自动化上位机通用框架:注册登录及主界面切换实现
后端·c#·.net
weixin_4837456239 分钟前
Springboot项目的目录结构
java·后端
阿里云云原生42 分钟前
2025年第二届“兴智杯”智能编码创新应用开发挑战赛正式启动
后端
米奇找不到妙妙屋1 小时前
部分请求报 CROS ERROR
spring boot·vue
Tirson Yang1 小时前
西安java面试总结1
java·面试
小猫咪怎么会有坏心思呢1 小时前
华为OD机试-猴子爬山-dp(JAVA 2025A卷)
java·算法·华为od
保持学习ing1 小时前
SpringBoot 前后台交互 -- CRUD
java·spring boot·后端·ssm·项目实战·页面放行