基于SpringBoot實現(xiàn)抽獎活動的四種策略
一、基于內(nèi)存的簡單抽獎策略
1.1 基本原理
最簡單的抽獎策略是將所有獎品信息加載到內(nèi)存中,通過隨機數(shù)算法從獎品池中選取一個獎品。
這種方式實現(xiàn)簡單,適合獎品種類少、規(guī)則簡單的小型抽獎活動。
1.2 實現(xiàn)方式
首先定義獎品實體:
@Data
public class Prize {
private Long id;
private String name;
private String description;
private Integer probability; // 中獎概率,1-10000之間的數(shù)字,表示萬分之幾
private Integer stock; // 庫存
private Boolean available; // 是否可用
}
然后實現(xiàn)抽獎服務(wù):
@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元優(yōu)惠券");
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之間的隨機數(shù)
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,設(shè)置為不可用
if (prize.getStock() <= 0) {
prize.setAvailable(false);
}
return prize;
}
}
// 如果所有獎品都不可用,返回默認(rèn)獎品
return getDefaultPrize();
}
private Prize getDefaultPrize() {
for (Prize prize : prizePool) {
if (prize.getName().equals("謝謝參與")) {
return prize;
}
}
// 創(chuàng)建一個默認(rèn)獎品
Prize defaultPrize = new Prize();
defaultPrize.setId(999L);
defaultPrize.setName("謝謝參與");
defaultPrize.setDescription("再接再厲");
return defaultPrize;
}
}
控制器實現(xiàn):
@RestController
@RequestMapping("/api/draw")
public class DrawController {
@Autowired
private SimpleDrawService drawService;
@GetMapping("/simple")
public Prize simpleDraw() {
return drawService.draw();
}
}
1.3 優(yōu)缺點分析
優(yōu)點:
- 實現(xiàn)簡單,開發(fā)成本低
- 無需數(shù)據(jù)庫支持,啟動即可使用
缺點:
- 不適合大規(guī)模并發(fā)場景
- 服務(wù)重啟后數(shù)據(jù)丟失,無法保證獎品總量控制
- 難以實現(xiàn)用戶抽獎次數(shù)限制和作弊防護(hù)
1.4 適用場景
- 小型活動或測試環(huán)境
- 獎品總量不敏感的場景
- 單機部署的簡單應(yīng)用
- 對抽獎公平性要求不高的場景
二、基于數(shù)據(jù)庫的抽獎策略
2.1 基本原理
將獎品信息、抽獎記錄等數(shù)據(jù)存儲在數(shù)據(jù)庫中,通過數(shù)據(jù)庫事務(wù)來保證獎品庫存的準(zhǔn)確性和抽獎記錄的完整性。
這種方式適合需要持久化數(shù)據(jù)并且對獎品庫存有嚴(yán)格管理要求的抽獎活動。
2.2 實現(xiàn)方式
數(shù)據(jù)庫表設(shè)計:
-- 獎品表
CREATE TABLE prize (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description VARCHAR(255),
probability INT NOT NULL COMMENT '中獎概率,1-10000之間的數(shù)字,表示萬分之幾',
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 '結(jié)束時間',
daily_limit INT DEFAULT 1 COMMENT '每人每日抽獎次數(shù)限制',
total_limit INT DEFAULT 10 COMMENT '每人總抽獎次數(shù)限制',
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 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 接口:
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();
}
服務(wù)實現(xiàn):
@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");
}
// 檢查用戶抽獎次數(shù)限制
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();
// 生成隨機數(shù)
int randomNum = random.nextInt(totalProbability) + 1;
// 根據(jù)概率選擇獎品
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) {
// 檢查每日抽獎次數(shù)限制
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");
}
// 檢查總抽獎次數(shù)限制
long totalDraws = drawRecordRepository.countByUserId(userId);
if (totalDraws >= activity.getTotalLimit()) {
throw new RuntimeException("Total draw limit exceeded");
}
}
}
控制器實現(xiàn):
@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 優(yōu)缺點分析
優(yōu)點:
- 數(shù)據(jù)持久化,服務(wù)重啟不丟失
- 可靠的庫存管理和抽獎記錄
- 支持用戶抽獎次數(shù)限制和活動時間控制
- 易于擴展其他業(yè)務(wù)需求
缺點:
- 數(shù)據(jù)庫操作帶來的性能開銷
- 高并發(fā)場景下可能出現(xiàn)數(shù)據(jù)庫瓶頸
- 實現(xiàn)相對復(fù)雜,開發(fā)成本較高
2.4 適用場景
- 中小型抽獎活動
- 需要精確控制獎品庫存的場景
- 需要完整抽獎記錄和數(shù)據(jù)分析的場景
三、基于Redis的高性能抽獎策略
3.1 基本原理
利用Redis的高性能和原子操作特性來實現(xiàn)抽獎系統(tǒng),將獎品信息和庫存存儲在Redis中,通過Lua腳本實現(xiàn)原子抽獎操作。這種方式適合高并發(fā)抽獎場景,能夠提供極高的性能和可靠的數(shù)據(jù)一致性。
3.2 實現(xiàn)方式
首先配置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;
}
}
抽獎服務(wù)實現(xiàn):
@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() {
// 初始化獎品數(shù)據(jù)
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元優(yōu)惠券");
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腳本進(jìn)行原子抽獎操作
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中執(zhí)行復(fù)雜對象有限制,我們這里簡化處理,使用Java代碼模擬
// 實際生產(chǎn)環(huán)境建議使用更細(xì)粒度的Redis數(shù)據(jù)結(jié)構(gòu)和腳本
// 模擬抽獎邏輯
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;
}
}
// 默認(rèn)返回最后一個獎品(通常是"謝謝參與")
return prizes.get(prizes.size() - 1);
}
private void checkDrawLimits(Long userId) {
// 檢查每日抽獎次數(shù)
String dailyKey = DAILY_DRAW_COUNT_KEY + userId + ":" + LocalDate.now();
Integer dailyCount = (Integer) redisTemplate.opsForValue().get(dailyKey);
if (dailyCount != null && dailyCount >= 3) { // 假設(shè)每日限制3次
throw new RuntimeException("Daily draw limit exceeded");
}
// 檢查總抽獎次數(shù)
String totalKey = TOTAL_DRAW_COUNT_KEY + userId;
Integer totalCount = (Integer) redisTemplate.opsForValue().get(totalKey);
if (totalCount != null && totalCount >= 10) { // 假設(shè)總限制10次
throw new RuntimeException("Total draw limit exceeded");
}
}
private void incrementUserDrawCount(Long userId) {
// 增加每日抽獎次數(shù)
String dailyKey = DAILY_DRAW_COUNT_KEY + userId + ":" + LocalDate.now();
redisTemplate.opsForValue().increment(dailyKey, 1);
// 設(shè)置過期時間(第二天凌晨過期)
long secondsUntilTomorrow = ChronoUnit.SECONDS.between(
LocalDateTime.now(),
LocalDate.now().plusDays(1).atStartOfDay());
redisTemplate.expire(dailyKey, secondsUntilTomorrow, TimeUnit.SECONDS);
// 增加總抽獎次數(shù)
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);
}
}
控制器實現(xiàn):
@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 優(yōu)缺點分析
優(yōu)點:
- 極高的性能,支持高并發(fā)場景
- 原子操作保證數(shù)據(jù)一致性
- 內(nèi)存操作,響應(yīng)速度快
- Redis持久化保證數(shù)據(jù)不丟失
缺點:
- 實現(xiàn)復(fù)雜度較高,尤其是Lua腳本部分
- 依賴Redis
- 可能需要定期同步數(shù)據(jù)到數(shù)據(jù)庫
3.4 適用場景
- 高并發(fā)抽獎活動
- 對響應(yīng)速度要求較高的場景
- 大型營銷活動
- 需要實時庫存控制的抽獎系統(tǒng)
四、基于權(quán)重概率的抽獎策略
4.1 基本原理
基于權(quán)重概率的抽獎策略是在普通抽獎基礎(chǔ)上增加了更復(fù)雜的概率計算邏輯,可以根據(jù)用戶特征、活動規(guī)則動態(tài)調(diào)整獎品中獎概率。
例如,可以根據(jù)用戶等級、消費金額、活動參與度等因素調(diào)整抽獎權(quán)重,實現(xiàn)精細(xì)化控制。
4.2 實現(xiàn)方式
首先定義動態(tài)權(quán)重計算接口:
public interface WeightCalculator {
// 根據(jù)用戶信息計算權(quán)重調(diào)整因子
double calculateWeightFactor(Long userId);
}
// VIP用戶權(quán)重計算器
@Component
public class VipWeightCalculator implements WeightCalculator {
@Autowired
private UserService userService;
@Override
public double calculateWeightFactor(Long userId) {
User user = userService.getUserById(userId);
// 根據(jù)用戶VIP等級調(diào)整權(quán)重
switch (user.getVipLevel()) {
case 0: return 1.0; // 普通用戶,不調(diào)整
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;
}
}
}
// 新用戶權(quán)重計算器
@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;
}
}
// 活躍度權(quán)重計算器
@Component
public class ActivityWeightCalculator implements WeightCalculator {
@Autowired
private UserActivityService userActivityService;
@Override
public double calculateWeightFactor(Long userId) {
int activityScore = userActivityService.getActivityScore(userId);
// 根據(jù)活躍度調(diào)整權(quán)重
if (activityScore >= 100) {
return 1.3; // 提高30%中獎率
} else if (activityScore >= 50) {
return 1.1; // 提高10%中獎率
}
return 1.0;
}
}
然后實現(xiàn)基于權(quán)重的抽獎服務(wù):
@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");
}
// 計算用戶的總權(quán)重因子
double weightFactor = calculateTotalWeightFactor(userId);
// 創(chuàng)建帶權(quán)重的獎品列表
List<WeightedPrize> weightedPrizes = createWeightedPrizeList(availablePrizes, weightFactor);
// 根據(jù)權(quán)重選擇獎品
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) {
// 從所有權(quán)重計算器獲取權(quán)重并相乘
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);
// 調(diào)整中獎概率
if (prize.getName().equals("謝謝參與")) {
// 對于"謝謝參與",權(quán)重因子反向作用(權(quán)重越高,越不容易"謝謝參與")
weightedPrize.setAdjustedProbability((int) (prize.getProbability() / weightFactor));
} else {
// 對于實際獎品,權(quán)重因子正向作用(權(quán)重越高,越容易中獎)
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();
// 生成隨機數(shù)
int randomNum = random.nextInt(totalProbability) + 1;
// 根據(jù)概率選擇獎品
int probabilitySum = 0;
for (WeightedPrize weightedPrize : weightedPrizes) {
probabilitySum += weightedPrize.getAdjustedProbability();
if (randomNum <= probabilitySum) {
return weightedPrize.getPrize();
}
}
// 默認(rèn)返回最后一個獎品(通常是"謝謝參與")
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);
}
// 帶權(quán)重的獎品類
@Data
private static class WeightedPrize {
private Prize prize;
private int adjustedProbability;
}
}
控制器實現(xiàn):
@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 優(yōu)缺點分析
優(yōu)點:
- 支持根據(jù)用戶特征和業(yè)務(wù)規(guī)則動態(tài)調(diào)整中獎概率
- 可以實現(xiàn)精細(xì)化營銷和用戶激勵
- 提高高價值用戶的體驗和留存
- 靈活的權(quán)重計算機制,易于擴展
缺點:
- 邏輯復(fù)雜,實現(xiàn)和維護(hù)成本高
- 可能影響抽獎公平性,需要謹(jǐn)慎處理
- 需要收集和分析更多用戶數(shù)據(jù)
4.4 適用場景
- 需要精細(xì)化運營的大型營銷活動
- 用戶分層明顯的應(yīng)用
- 希望提高特定用戶群體體驗的場景
- 有用戶激勵和留存需求的平臺
五、方案對比
6.1 性能對比
| 抽獎策略 | 響應(yīng)速度 | 并發(fā)支持 | 資源消耗 | 擴展性 |
|---|---|---|---|---|
| 內(nèi)存抽獎 | 極快 | 低 | 低 | 低 |
| 數(shù)據(jù)庫抽獎 | 中等 | 中等 | 中等 | 高 |
| Redis抽獎 | 快 | 高 | 中等 | 高 |
| 權(quán)重抽獎 | 中等 | 中等 | 高 | 高 |
6.2 功能對比
| 抽獎策略 | 獎品管理 | 抽獎記錄 | 用戶限制 | 防作弊 | 定制性 |
|---|---|---|---|---|---|
| 內(nèi)存抽獎 | 基礎(chǔ) | 無 | 無 | 無 | 低 |
| 數(shù)據(jù)庫抽獎 | 完善 | 完善 | 支持 | 基礎(chǔ) | 中等 |
| Redis抽獎 | 完善 | 完善 | 支持 | 中等 | 高 |
| 權(quán)重抽獎 | 完善 | 完善 | 支持 | 高 | 極高 |
六、結(jié)語
在實際項目中,我們需要根據(jù)業(yè)務(wù)需求、用戶規(guī)模、性能要求等因素,選擇合適的抽獎策略或組合多種策略,以構(gòu)建高效、可靠、安全的抽獎系統(tǒng)。
無論選擇哪種抽獎策略,都需要關(guān)注系統(tǒng)的公平性、性能、可靠性和安全性,不斷優(yōu)化和改進(jìn)。
以上就是基于SpringBoot實現(xiàn)抽獎活動的四種策略的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot抽獎活動的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
客戶端Socket與服務(wù)端ServerSocket串聯(lián)實現(xiàn)網(wǎng)絡(luò)通信
這篇文章主要為大家介紹了客戶端Socket與服務(wù)端ServerSocket串聯(lián)實現(xiàn)網(wǎng)絡(luò)通信的內(nèi)容詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03
java 如何遠(yuǎn)程控制tomcat啟動關(guān)機
這篇文章主要介紹了java 遠(yuǎn)程控制tomcat啟動關(guān)機的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04
IDEA解決maven包沖突easypoi NoClassDefFoundError的問題
這篇文章主要介紹了IDEA解決maven包沖突easypoi NoClassDefFoundError的問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
解決IntelliJ IDEA中鼠標(biāo)拖動選擇為矩形區(qū)域問題
這篇文章主要介紹了解決IntelliJ IDEA中鼠標(biāo)拖動選擇為矩形區(qū)域問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10
springCloud gateWay 統(tǒng)一鑒權(quán)的實現(xiàn)代碼
這篇文章主要介紹了springCloud gateWay 統(tǒng)一鑒權(quán)的實現(xiàn)代碼,統(tǒng)一鑒權(quán)包括鑒權(quán)邏輯和代碼實現(xiàn),本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02
使用Java構(gòu)造和解析Json數(shù)據(jù)的兩種方法(詳解二)
這篇文章主要介紹了使用Java構(gòu)造和解析Json數(shù)據(jù)的兩種方法(詳解二)的相關(guān)資料,需要的朋友可以參考下2016-03-03

