狐友抽奖平台构建之路
01
业务背景
01
业务背景
狐友是一款面向年轻人群体的社交APP,在初期阶段,当运营组织抽奖活动时,缺乏专门的系统,只能使用Feed抽奖,走线下流程发奖,过程非常繁琐。
而通过Feed抽奖的方式,是无法组织全平台的大型抽奖活动,因为Feed抽奖的形式是在评论区随机选取中奖用户,运营无法让全平台的用户去对一条Feed进行评论,从而组装抽奖活动的。
02
业务痛点
02
业务痛点
当运营组织节日平台抽奖活动(如春节)则需定制开发,耗时且难以复用,仅可以一次性使用,占用宝贵的研发和测试资源。
开发侧遇到的问题:
1.开发周期长,每次活动都要定制化进行开发,活动业务层并不复杂,但每次活动都要考虑系统安全性、稳定性、数据一致性问题;
运营侧遇到的问题:
测试侧遇到的问题:
基于上述背景,我们需要一个通用、灵活、可配置的抽奖活动模板中台化的系统,满足以下需求:
1.提供给运营/圈主,可灵活化配置,可自己完成活动配置上线,可复用,满足他们拉新、促活、留存的运营目标;
2.技术架构层面确保可复用性和可扩展性,沉淀能力可复用,拒绝重复开发,把可复用的能力沉淀;
3.适应未来业务变化,减少研发和测试工作量。
03
方案设计
03
方案设计
3.1 数据模型定义
明确了业务需求,接下来需要对核心业务模型进行抽象,我们的运营策略是以互动玩法任务为抓手,以活动抽奖为核心的模式,抽奖的奖品设置多级奖等梯度策略,希望激发用户的参与热情,多玩多抽奖,增加用户粘性,基于以上,设计了如下的数据模型:
在设计之初,明确了以奖等-奖品-玩法的核心模型,各部分采用松耦合的关联方式,这套模型的优势在于模块化的设计,相互独立,适合微服务架构,以此达到高内聚低耦合的目标。
以奖等为例,每个抽奖活动均可以配置多级奖等,例如一等奖、二等奖、N等奖,每级奖等可以配置其规则策略,如中奖概率、发放规则(动态时段投放、用户画像投放等)、库存数量等。
奖品则附属于奖等,同一个奖品可以配置在不同的奖等当中,奖品亦可以配置其规则策略,如限中策略、库存、发放规则等。
业务玩法则置于最上层,与奖等奖品解耦,可以针对不同的活动设计不同的玩法逻辑,吸引用户参与,例如每年天猫双十一的互动玩法都不一样,可以给用户带来新鲜感,吸引用户参与互动,增加平台活跃度和用户粘性。
3.2 业务架构
基于以上的分析,结合狐友的实际业务场景,我们设计了如上的系统架构,总体分为几个部分:
1.业务玩法活动:针对每次不同的运营活动,H5端支持多种不同的玩法组件,运营可以自行选择本次活动希望使用的玩法形式,配置活动物料信息,即可准备完成,大大的减少了运营人员组织活动的难度;
2.核心业务系统:该部分作为整个系统的核心模块,包括了活动的配置信息、C端API服务、数据报表监控等,运营人员可以通过运营后台界面完成活动的配置,其中主要包括活动基础信息配置、活动奖项配置、奖项规则&概率配置、奖品配置、库存配置、玩法任务规则配置等。
其中关键模块设计,如下几点:
奖项配置:奖项与奖品相互独立,奖项可以采用灵活配置策略,支持单一奖品和多奖品礼包多种组合态的发放模式;
奖品管理:采用独立库存设计,与奖项的库存分离,奖品的库存以独立的库存池的形式,奖项的库存使用预占库存的形式从奖品库存池中划拨,这样做的好处,一方面方便运营BD人员统一管理奖品权益,另一方面以应对高并发场景下的弹性扩展;
各模块之间可灵活多变的组合策略,可以满足不同业务需求,达成高内聚松耦合的设计目标。
3.外部系统集成:结合业务规则与这些已有的系统能力,开发了多种业务玩法:
任务系统是玩法的一个重要依赖,其提供了用户行为任务、签到任务、分享任务等任务编排的能力; 用户行为系统是任务系统的核心组成部分,通过客户端用户行为埋点上报,可以为任务系统提供更加丰富的类型; Feed Server是狐友核心Feed流服务,也是狐友基石,为活动的分享、社交裂变、玩法互动提供了重要支持。
业务流程
系统时序图
上图就是整体系统的业务流程图,这里简单进行介绍:
运营人员配置活动基础参数(活动信息、奖项、奖品、库存、任务规则等); 设定活动的进行周期及其活动物料; 完成配置,发布活动。
进入活动页面,参与活动玩法,完成互动任务完成 → 获得抽奖机会; 进行抽奖 → 中奖 → 填写中奖信息(仅实物奖品) → 奖品发放。
实时监控活动数据; 动态调整活动参数,如任务信息、奖项中奖概率、奖品库存、活动物料图片等; 管理活动中奖名单及奖品发放流程。
设计要点
1.预占库存
在实际的业务场景下,运营人员分为两种角色,活动运营人员与运营BD人员,前者主要负责活动的策划和执行层面,后者负责活动奖品的商务拓展,与外部商家对接,获得商家的赞助。
两种运营的角色不同,关注的业务重点也不一样,BD运营关注奖品的获取,BD奖品后,希望将奖品导入统一的资源池中管理;活动运营聚焦将有限的奖品合理分配到不同的活动中去。
结合以上实际场景,我们引入预占库存的逻辑,将奖品分为总资源池和活动资源池,逻辑上相互独立,当新建一个玩法活动配置奖品库存时,需要进行预占库存的操作,会从总资源池中预占一部分库存,作为该活动的奖品库存,当活动的奖品库存发放完时,活动运营可以选择追加库存,当活动结束时,如果奖品库存仍有剩余,会自动返还至总资源池。
2.多租户数据隔离
平台在设计之初,考虑到业务的使用场景,并不仅仅针对本部门内部,而是可以对多业务部门开发使用,因此在业务数据层面,需支持多租户的数据隔离,保证不同的账号仅可以操作自己名下的数据,保证数据的安全性。
3.3 技术挑战
在设计和实现狐友抽奖活动系统时,需要解决一系列技术难题,以确保系统在高并发场景下的稳定性、公平性以及运营需求的灵活性。以下是四个核心技术挑战及其解决方案:
1.分桶库存的扣减逻辑
背景
狐友抽奖活动系统采用了预占库存的机制,将奖品库存分为总资源池和活动资源池。运营BD人员负责将奖品导入总资源池,而活动运营人员在配置活动时,从总资源池中预占一部分库存作为活动库存池。活动结束后,未使用的库存会返还至总资源池。这种分桶设计提高了库存管理的灵活性,但也带来了技术上的复杂性。
挑战
高并发下的库存扣减:活动高峰期,大量用户同时抽奖,可能导致库存扣减的并发冲突; 库存准确性与一致性:需确保库存扣减的准确性,避免出现超发或少发的情况; 分桶设计的复杂性:同一个奖品可能被多个活动共享,如何在多个活动库存桶之间合理分配和扣减库存是一个难点。
解决方案
分桶设计:每个活动拥有独立的库存桶,从总资源池预占库存到活动库存桶,逻辑上隔离不同活动的库存管理; 乐观锁机制:在扣减库存时,使用乐观锁(如数据库的版本号或CAS操作),每次扣减前检查当前库存是否足够,若足够则扣减,否则返回失败; 分布式锁:对于需要跨服务操作总资源池的场景,使用分布式锁(如基于Redis或ZooKeeper)确保同一时间只有一个请求修改库存; 库存预热:活动开始前,提前完成库存预占操作,避免活动期间频繁访问总资源池,提升性能; 异步扣减:对于非实时性要求高的场景,采用消息队列异步处理库存扣减,降低对用户请求的响应延迟。
示例
假设奖品A的总资源池有1000个库存,活动1预占200个,活动2预占300个,则活动1库存桶有200个,活动2库存桶有300个,总资源池剩余500个。当活动1中一名用户中奖时,系统从活动1的库存桶扣减1个,若库存不足,则该用户无法中奖。若活动1结束后剩余50个库存,则这50个库存自动返还至总资源池。
这里给出核心逻辑的伪代码实现,仅供参考:
@Slf4j
@Service
publicclassStockService{
privatefinal PrizeRepository prizeRepository;
privatefinal ActivityStockRepository activityStockRepository;
privatefinal RedissonClient redissonClient;
privatefinal RedisTemplate<String, Object> redisTemplate;
privatefinal RabbitTemplate rabbitTemplate;
/**
* 从总资源池预占库存到活动资源池
* @param activityId 活动ID
* @param prizeId 奖品ID
* @param quantity 预占数量
* @return 是否预占成功
*/
@Transactional
publicbooleanpreOccupyStock(Long activityId, Long prizeId, int quantity){
// 使用分布式锁确保同一时间只有一个请求修改总资源池
RLock lock = redissonClient.getLock("stock:total:" + prizeId);
try {
// 尝试获取锁,最多等待5秒,锁过期时间30秒
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
// 查询总资源池库存
Prize prize = prizeRepository.findById(prizeId)
.orElseThrow(() -> new RuntimeException("奖品不存在"));
// 检查总资源池库存是否足够
if (prize.getAvailableStock() < quantity) {
log.warn("总资源池库存不足,prizeId={}, 当前库存={}, 需要预占={}",
prizeId, prize.getAvailableStock(), quantity);
returnfalse;
}
// 使用乐观锁更新总资源池库存
int updated = prizeRepository.decreaseStock(prizeId, quantity, prize.getVersion());
if (updated == 0) {
log.warn("乐观锁更新失败,prizeId={}, version={}", prizeId, prize.getVersion());
returnfalse;
}
// 更新或创建活动库存桶
ActivityStock activityStock = activityStockRepository
.findByActivityIdAndPrizeId(activityId, prizeId)
.orElse(new ActivityStock(activityId, prizeId, 0));
activityStock.setStock(activityStock.getStock() + quantity);
activityStockRepository.save(activityStock);
log.info("预占库存成功,activityId={}, prizeId={}, quantity={}",
activityId, prizeId, quantity);
returntrue;
} finally {
lock.unlock();
}
} else {
log.warn("获取分布式锁超时,activityId={}, prizeId={}", activityId, prizeId);
returnfalse;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("预占库存过程被中断", e);
returnfalse;
}
}
/**
* 活动结束后,将未使用的库存返还至总资源池
* @param activityId 活动ID
* @return 是否返还成功
*/
@Transactional
publicbooleanreturnUnusedStock(Long activityId){
List<ActivityStock> activityStocks = activityStockRepository.findByActivityId(activityId);
for (ActivityStock activityStock : activityStocks) {
if (activityStock.getStock() <= 0) {
continue;
}
RLock lock = redissonClient.getLock("stock:total:" + activityStock.getPrizeId());
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
// 返还库存到总资源池
Prize prize = prizeRepository.findById(activityStock.getPrizeId())
.orElseThrow(() -> new RuntimeException("奖品不存在"));
prize.setAvailableStock(prize.getAvailableStock() + activityStock.getStock());
prizeRepository.save(prize);
// 清空活动库存
activityStock.setStock(0);
activityStockRepository.save(activityStock);
log.info("返还未使用库存成功,activityId={}, prizeId={}, quantity={}",
activityId, activityStock.getPrizeId(), activityStock.getStock());
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("返还库存过程被中断", e);
returnfalse;
}
}
returntrue;
}
/**
* 用户抽奖时扣减活动库存
* @param activityId 活动ID
* @param prizeId 奖品ID
* @return 是否扣减成功
*/
publicbooleandecreaseActivityStock(Long activityId, Long prizeId){
// 使用Redis缓存活动库存,提高并发性能
String stockKey = "stock:activity:" + activityId + ":" + prizeId;
// 使用Redis的原子操作进行库存扣减
Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
// 如果Redis中没有缓存,则从数据库加载并缓存
if (remainStock == null) {
// 加载活动库存到Redis
return preloadActivityStock(activityId, prizeId);
} elseif (remainStock < 0) {
// 库存不足,回滚操作
redisTemplate.opsForValue().increment(stockKey);
returnfalse;
}
// 异步更新数据库中的库存
rabbitTemplate.convertAndSend("stock.exchange", "stock.decrease",
new StockMessage(activityId, prizeId, 1));
returntrue;
}
/**
* 预热活动库存到Redis
* @param activityId 活动ID
* @param prizeId 奖品ID
* @return 是否预热成功
*/
publicbooleanpreloadActivityStock(Long activityId, Long prizeId){
ActivityStock activityStock = activityStockRepository
.findByActivityIdAndPrizeId(activityId, prizeId)
.orElseThrow(() -> new RuntimeException("活动库存不存在"));
String stockKey = "stock:activity:" + activityId + ":" + prizeId;
redisTemplate.opsForValue().set(stockKey, activityStock.getStock());
log.info("预热活动库存成功,activityId={}, prizeId={}, stock={}",
activityId, prizeId, activityStock.getStock());
returntrue;
}
/**
* 异步处理库存扣减消息
* @param message 库存消息
*/
@Transactional
publicvoidhandleStockDecrease(StockMessage message){
ActivityStock activityStock = activityStockRepository
.findByActivityIdAndPrizeId(message.getActivityId(), message.getPrizeId())
.orElseThrow(() -> new RuntimeException("活动库存不存在"));
// 使用乐观锁更新库存
int updated = activityStockRepository.decreaseStock(
message.getActivityId(), message.getPrizeId(),
message.getQuantity(), activityStock.getVersion());
if (updated == 0) {
log.warn("乐观锁更新活动库存失败,重试中...");
// 可以实现重试逻辑
}
}
}
/**
* 奖品仓库接口
*/
@Repository
publicinterfacePrizeRepositoryextendsJpaRepository<Prize, Long> {
/**
* 使用乐观锁扣减总资源池库存
*/
@Modifying
@Query("UPDATE Prize p SET p.availableStock = p.availableStock - ?2, p.version = p.version + 1 " +
"WHERE p.id = ?1 AND p.availableStock >= ?2 AND p.version = ?3")
intdecreaseStock(Long prizeId, int quantity, int version);
}
/**
* 活动库存仓库接口
*/
@Repository
publicinterfaceActivityStockRepositoryextendsJpaRepository<ActivityStock, Long> {
/**
* 根据活动ID和奖品ID查询活动库存
*/
Optional<ActivityStock> findByActivityIdAndPrizeId(Long activityId, Long prizeId);
/**
* 根据活动ID查询所有活动库存
*/
List<ActivityStock> findByActivityId(Long activityId);
/**
* 使用乐观锁扣减活动库存
*/
@Modifying
@Query("UPDATE ActivityStock a SET a.stock = a.stock - ?3, a.version = a.version + 1 " +
"WHERE a.activityId = ?1 AND a.prizeId = ?2 AND a.stock >= ?3 AND a.version = ?4")
intdecreaseStock(Long activityId, Long prizeId, int quantity, int version);
}
2.用户风控
背景
抽奖活动中,用户作弊行为(如脚本刷奖、注册小号参与)会损害活动的公平性,影响正常用户的体验和运营效果。系统需要实时识别和限制此类行为,同时保持对正常用户的友好性。
挑战
作弊用户识别:如何准确区分作弊用户和正常用户,避免误判; 实时风控:在用户参与抽奖的瞬间完成风控判断,响应时间需足够快; 策略灵活性:不同活动可能需要不同的风控规则,系统需支持动态配置。
解决方案
用户行为分析:通过分析用户行为数据(如登录频率、抽奖频率、设备切换等),识别异常模式; 设备指纹:利用设备指纹技术,检测同一设备下的多个账号,限制多账号作弊; IP限制:限制同一IP地址的参与次数,防止批量操作; 验证码验证:在抽奖等关键环节加入验证码,阻止脚本自动化行为; 风控规则引擎:设计一个可配置的风控规则引擎,支持运营设置规则,如每日参与次数上限、黑名单用户限制等; 实时监控:通过后台实时监控参与数据,发现异常(如某用户短时间内高频抽奖)时及时干预。
示例
假设设置规则:每用户每日最多抽奖3次。若某用户在5分钟内尝试抽奖10次,系统检测到异常行为,将其列入临时黑名单,限制其继续参与。同时,若发现某设备关联了多个账号,且抽奖频率异常,则限制该设备的所有账号参与。
3.超发控制
背景
超发是指奖品发放数量超出预设库存,可能因高并发下的并发冲突或系统错误导致。这不仅增加运营成本,还可能引发用户投诉,损害平台信誉。
挑战
高并发库存控制:大量用户同时抽奖时,如何确保库存不被超扣; 分布式一致性:在分布式系统中,各节点对库存的扣减需保持一致; 库存回滚:若用户中奖后放弃领奖,需将库存回滚至活动库存桶。
解决方案
数据库事务:在扣减库存时使用事务,确保扣减操作的原子性; Redis原子操作:利用Redis的原子命令(如DECR),在内存中高效完成库存扣减; 库存预扣:用户抽奖时,先预扣库存,若中奖则确认扣减,未中奖则回滚预扣; 消息队列:通过消息队列异步处理库存扣减请求,平滑高并发压力; 监控与报警:实时监控库存余量,当接近耗尽时触发报警,必要时暂停活动。
示例
用户点击抽奖时,系统检查活动库存桶剩余数量。若有库存,则预扣1个并执行抽奖逻辑:中奖则确认扣减,未中奖则回滚。若Redis中库存计数器显示余量为0,则直接返回“奖品已发完”,避免超发。
4.中奖概率测算
背景
中奖概率是抽奖活动的核心参数,由运营人员配置,直接影响活动效果。系统需确保实际中奖率与配置概率一致,同时保证随机性和公平性。
挑战
概率准确性:在大量抽奖中,实际中奖率需接近配置值; 随机性保障:抽奖结果需无明显规律,避免用户感知到不公平; 动态调整:活动中可能需根据库存和参与情况调整概率。
解决方案
伪随机算法:使用高质量伪随机数生成器(如Java的SecureRandom),确保随机性; 分时段概率:将活动周期划分为多个时段,根据剩余库存和时间动态调整概率,保证库存合理消耗; 蓄水池算法:对于固定中奖人数的活动,采用蓄水池抽样算法,确保随机性和公平性; 实时统计:监控实际中奖率,若偏离配置值过多,动态调整随机数范围; 种子管理:为每次抽奖生成独立种子(如基于用户ID和时间戳),避免结果可预测。
示例
假设活动配置1%的中奖概率,系统生成0-999的随机数,若结果为0则中奖。在活动初期,若库存充足,保持1%概率;若库存接近耗尽,可降低概率至0.5%,通过后台动态调整,确保库存发放符合预期。
这里给出核心逻辑的伪代码实现,仅供参考:
/**
* 抽奖概率服务
* 负责控制抽奖活动的中奖概率,确保实际中奖率与配置概率一致
*/
@Slf4j
@Service
publicclassLotteryProbabilityService{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private LotteryActivityRepository activityRepository;
@Autowired
private LotteryPrizeRepository prizeRepository;
@Autowired
private LotteryRecordRepository recordRepository;
// 缓存活动的实时中奖统计
privatefinal Map<Long, ProbabilityStatistics> activityStatisticsMap = new ConcurrentHashMap<>();
/**
* 根据配置概率进行抽奖
*
* @param activityId 活动ID
* @param userId 用户ID
* @return 中奖的奖品ID,如果未中奖则返回null
*/
public Long draw(Long activityId, String userId){
// 获取活动信息
LotteryActivity activity = activityRepository.findById(activityId)
.orElseThrow(() -> new RuntimeException("活动不存在"));
// 检查活动是否在进行中
if (!isActivityInProgress(activity)) {
thrownew RuntimeException("活动未开始或已结束");
}
// 获取活动奖品列表
List<LotteryPrize> prizes = prizeRepository.findByActivityId(activityId);
if (prizes.isEmpty()) {
thrownew RuntimeException("活动没有配置奖品");
}
// 获取活动统计信息,如果不存在则创建
ProbabilityStatistics statistics = activityStatisticsMap.computeIfAbsent(
activityId, k -> new ProbabilityStatistics(activityId));
// 根据活动阶段动态调整概率
adjustProbabilityByStage(activity, prizes, statistics);
// 生成随机数种子(基于用户ID和时间戳)
long seed = generateSeed(userId, activityId);
SecureRandom random = new SecureRandom();
random.setSeed(seed);
// 执行抽奖
Long prizeId = doDraw(random, prizes, statistics);
// 记录抽奖结果并更新统计
recordDrawResult(activityId, userId, prizeId, statistics);
return prizeId;
}
/**
* 使用蓄水池算法进行固定中奖人数的抽奖
*
* @param activityId 活动ID
* @param totalParticipants 总参与人数
* @param winnerCount 中奖人数
* @return 中奖用户ID列表
*/
public List<String> reservoirSampling(Long activityId, List<String> participants, int winnerCount){
if (participants.size() <= winnerCount) {
returnnew ArrayList<>(participants);
}
// 生成活动特定的随机种子
long seed = activityId * System.currentTimeMillis();
ThreadLocalRandom random = ThreadLocalRandom.current();
random.setSeed(seed);
// 蓄水池算法实现
List<String> winners = new ArrayList<>(winnerCount);
// 前winnerCount个元素直接放入结果集
for (int i = 0; i < winnerCount; i++) {
winners.add(participants.get(i));
}
// 对于后续元素,以概率i/(j+1)决定是否替换结果集中的元素
for (int j = winnerCount; j < participants.size(); j++) {
int i = random.nextInt(j + 1);
if (i < winnerCount) {
winners.set(i, participants.get(j));
}
}
return winners;
}
/**
* 根据活动阶段动态调整概率
*/
privatevoidadjustProbabilityByStage(LotteryActivity activity, List<LotteryPrize> prizes,
ProbabilityStatistics statistics){
// 计算活动已进行的比例
double progressRatio = calculateActivityProgress(activity);
// 计算奖品消耗比例
for (LotteryPrize prize : prizes) {
if (prize.getStock() <= 0) {
continue; // 库存为0的奖品跳过
}
double consumptionRatio = calculatePrizeConsumption(prize);
// 根据进度和消耗比例调整概率
if (consumptionRatio < progressRatio * 0.8) {
// 消耗太慢,提高概率
prize.setAdjustedProbability(prize.getProbability() * 1.2);
} elseif (consumptionRatio > progressRatio * 1.2) {
// 消耗太快,降低概率
prize.setAdjustedProbability(prize.getProbability() * 0.8);
} else {
// 消耗正常,使用原概率
prize.setAdjustedProbability(prize.getProbability());
}
// 检查实际中奖率与配置概率的偏差
double actualProbability = statistics.getActualProbability(prize.getId());
if (Math.abs(actualProbability - prize.getProbability()) > 0.05) {
// 偏差过大,进一步调整
if (actualProbability > prize.getProbability()) {
prize.setAdjustedProbability(prize.getAdjustedProbability() * 0.9);
} else {
prize.setAdjustedProbability(prize.getAdjustedProbability() * 1.1);
}
}
log.debug("奖品[{}]调整后概率: {}", prize.getName(), prize.getAdjustedProbability());
}
// 归一化调整后的概率,确保总和为1
normalizeProbabilities(prizes);
}
/**
* 归一化概率,确保总和为1
*/
privatevoidnormalizeProbabilities(List<LotteryPrize> prizes){
double sum = 0;
for (LotteryPrize prize : prizes) {
if (prize.getStock() > 0) {
sum += prize.getAdjustedProbability();
}
}
if (sum > 0) {
for (LotteryPrize prize : prizes) {
if (prize.getStock() > 0) {
prize.setAdjustedProbability(prize.getAdjustedProbability() / sum);
}
}
}
}
/**
* 计算活动进度比例
*/
privatedoublecalculateActivityProgress(LotteryActivity activity){
long startTime = activity.getStartTime().toEpochSecond(ZoneOffset.UTC);
long endTime = activity.getEndTime().toEpochSecond(ZoneOffset.UTC);
long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
if (currentTime <= startTime) {
return0;
}
if (currentTime >= endTime) {
return1;
}
return (double) (currentTime - startTime) / (endTime - startTime);
}
/**
* 计算奖品消耗比例
*/
privatedoublecalculatePrizeConsumption(LotteryPrize prize){
int initialStock = prize.getInitialStock();
int currentStock = prize.getStock();
if (initialStock <= 0) {
return0;
}
return (double) (initialStock - currentStock) / initialStock;
}
/**
* 执行抽奖逻辑
*/
private Long doDraw(SecureRandom random, List<LotteryPrize> prizes, ProbabilityStatistics statistics){
double randomValue = random.nextDouble(); // 生成[0,1)之间的随机数
double cumulativeProbability = 0;
// 根据概率区间确定中奖奖品
for (LotteryPrize prize : prizes) {
if (prize.getStock() <= 0) {
continue; // 库存为0的奖品跳过
}
cumulativeProbability += prize.getAdjustedProbability();
if (randomValue < cumulativeProbability) {
return prize.getId();
}
}
// 未中奖
returnnull;
}
/**
* 生成随机数种子
*/
privatelonggenerateSeed(String userId, Long activityId){
String seedStr = userId + "_" + activityId + "_" + System.currentTimeMillis();
return seedStr.hashCode();
}
}
04
平台持续探索与展望
04
平台持续探索与展望
狐友抽奖平台的构建,不仅解决了初期业务中活动开发效率低、复用性差、运营流程复杂等问题,更通过中台化的设计理念,为未来的业务发展提供了灵活的技术支撑。在平台上线后,我们持续关注用户反馈、技术演进与业务场景的适配性,逐步探索以下方向,以推动平台的长期价值最大化。