搜狐技术产品

狐友抽奖平台构建之路

01

业务背景

狐友是一款面向年轻人群体的社交APP,在初期阶段,当运营组织抽奖活动时,缺乏专门的系统,只能使用Feed抽奖,走线下流程发奖,过程非常繁琐。

而通过Feed抽奖的方式,是无法组织全平台的大型抽奖活动,因为Feed抽奖的形式是在评论区随机选取中奖用户,运营无法让全平台的用户去对一条Feed进行评论,从而组装抽奖活动的。

Image

02

业务痛点

当运营组织节日平台抽奖活动(如春节)则需定制开发,耗时且难以复用,仅可以一次性使用,占用宝贵的研发和测试资源。

Image

开发侧遇到的问题:

1.开发周期长,每次活动都要定制化进行开发,活动业务层并不复杂,但每次活动都要考虑系统安全性、稳定性、数据一致性问题;

2.数据模型混乱,无法形成统一的规则,每一次都要与产品运营重新讨论业务定义及数据模型设计;
3.难以形成技术沉淀,无法形成统一化的平台开放给其他部门使用。

运营侧遇到的问题:

1.配置流程复杂,从前端页面配置到活动规则配置分散在多个系统;
2.沟通成本高,开发侧概念不统一,每次运营对接不同的开发都要接受一套概念;
3.活动效果无法实时关注,往往都是活动后才开始跑效果数据。

测试侧遇到的问题:

1.测试成本高,每次活动都要重新编写测试用例,在测试时需要构造多个测试用例条件的账户,用来测试不同情况,此时往往需要开发共同配合;
2.测试人力紧张,每次都要投入人力测试非常相似但是不一样的业务玩法。

基于上述背景,我们需要一个通用、灵活、可配置的抽奖活动模板中台化的系统,满足以下需求:

1.提供给运营/圈主,可灵活化配置,可自己完成活动配置上线,可复用,满足他们拉新、促活、留存的运营目标;

2.技术架构层面确保可复用性和可扩展性,沉淀能力可复用,拒绝重复开发,把可复用的能力沉淀;

3.适应未来业务变化,减少研发和测试工作量。

03

方案设计

3.1 数据模型定义

明确了业务需求,接下来需要对核心业务模型进行抽象,我们的运营策略是以互动玩法任务为抓手,以活动抽奖为核心的模式,抽奖的奖品设置多级奖等梯度策略,希望激发用户的参与热情,多玩多抽奖,增加用户粘性,基于以上,设计了如下的数据模型:

Image

在设计之初,明确了以奖等-奖品-玩法的核心模型,各部分采用松耦合的关联方式,这套模型的优势在于模块化的设计,相互独立,适合微服务架构,以此达到高内聚低耦合的目标。

以奖等为例,每个抽奖活动均可以配置多级奖等,例如一等奖、二等奖、N等奖,每级奖等可以配置其规则策略,如中奖概率、发放规则(动态时段投放、用户画像投放等)、库存数量等。

奖品则附属于奖等,同一个奖品可以配置在不同的奖等当中,奖品亦可以配置其规则策略,如限中策略、库存、发放规则等。

业务玩法则置于最上层,与奖等奖品解耦,可以针对不同的活动设计不同的玩法逻辑,吸引用户参与,例如每年天猫双十一的互动玩法都不一样,可以给用户带来新鲜感,吸引用户参与互动,增加平台活跃度和用户粘性。

3.2 业务架构

Image

基于以上的分析,结合狐友的实际业务场景,我们设计了如上的系统架构,总体分为几个部分:

1.业务玩法活动:针对每次不同的运营活动,H5端支持多种不同的玩法组件,运营可以自行选择本次活动希望使用的玩法形式,配置活动物料信息,即可准备完成,大大的减少了运营人员组织活动的难度;

2.核心业务系统:该部分作为整个系统的核心模块,包括了活动的配置信息、C端API服务、数据报表监控等,运营人员可以通过运营后台界面完成活动的配置,其中主要包括活动基础信息配置、活动奖项配置、奖项规则&概率配置、奖品配置、库存配置、玩法任务规则配置等。

其中关键模块设计,如下几点:

  • 奖项配置:奖项与奖品相互独立,奖项可以采用灵活配置策略,支持单一奖品和多奖品礼包多种组合态的发放模式;

  • 奖品管理:采用独立库存设计,与奖项的库存分离,奖品的库存以独立的库存池的形式,奖项的库存使用预占库存的形式从奖品库存池中划拨,这样做的好处,一方面方便运营BD人员统一管理奖品权益,另一方面以应对高并发场景下的弹性扩展;

  • 各模块之间可灵活多变的组合策略,可以满足不同业务需求,达成高内聚松耦合的设计目标。

3.外部系统集成:结合业务规则与这些已有的系统能力,开发了多种业务玩法:

  • 任务系统是玩法的一个重要依赖,其提供了用户行为任务、签到任务、分享任务等任务编排的能力;
  • 用户行为系统是任务系统的核心组成部分,通过客户端用户行为埋点上报,可以为任务系统提供更加丰富的类型;
  • Feed Server是狐友核心Feed流服务,也是狐友基石,为活动的分享、社交裂变、玩法互动提供了重要支持。

业务流程

Image

系统时序图

Image

上图就是整体系统的业务流程图,这里简单进行介绍:

1.活动初始化:
  • 运营人员配置活动基础参数(活动信息、奖项、奖品、库存、任务规则等);
  • 设定活动的进行周期及其活动物料;
  • 完成配置,发布活动。
2.用户参与玩法:
  • 进入活动页面,参与活动玩法,完成互动任务完成 → 获得抽奖机会;
  • 进行抽奖 → 中奖 → 填写中奖信息(仅实物奖品) → 奖品发放。
3.后台管理:
  • 实时监控活动数据;
  • 动态调整活动参数,如任务信息、奖项中奖概率、奖品库存、活动物料图片等;
  • 管理活动中奖名单及奖品发放流程。

设计要点

1.预占库存

在实际的业务场景下,运营人员分为两种角色,活动运营人员与运营BD人员,前者主要负责活动的策划和执行层面,后者负责活动奖品的商务拓展,与外部商家对接,获得商家的赞助。

两种运营的角色不同,关注的业务重点也不一样,BD运营关注奖品的获取,BD奖品后,希望将奖品导入统一的资源池中管理;活动运营聚焦将有限的奖品合理分配到不同的活动中去。

结合以上实际场景,我们引入预占库存的逻辑,将奖品分为总资源池和活动资源池,逻辑上相互独立,当新建一个玩法活动配置奖品库存时,需要进行预占库存的操作,会从总资源池中预占一部分库存,作为该活动的奖品库存,当活动的奖品库存发放完时,活动运营可以选择追加库存,当活动结束时,如果奖品库存仍有剩余,会自动返还至总资源池。

Image

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个库存自动返还至总资源池。

Image

这里给出核心逻辑的伪代码实现,仅供参考:

@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次,系统检测到异常行为,将其列入临时黑名单,限制其继续参与。同时,若发现某设备关联了多个账号,且抽奖频率异常,则限制该设备的所有账号参与。

Image

3.超发控制

背景

超发是指奖品发放数量超出预设库存,可能因高并发下的并发冲突或系统错误导致。这不仅增加运营成本,还可能引发用户投诉,损害平台信誉。

挑战

  • 高并发库存控制:大量用户同时抽奖时,如何确保库存不被超扣;
  • 分布式一致性:在分布式系统中,各节点对库存的扣减需保持一致;
  • 库存回滚:若用户中奖后放弃领奖,需将库存回滚至活动库存桶。

解决方案

  • 数据库事务:在扣减库存时使用事务,确保扣减操作的原子性;
  • Redis原子操作:利用Redis的原子命令(如DECR),在内存中高效完成库存扣减;
  • 库存预扣:用户抽奖时,先预扣库存,若中奖则确认扣减,未中奖则回滚预扣;
  • 消息队列:通过消息队列异步处理库存扣减请求,平滑高并发压力;
  • 监控与报警:实时监控库存余量,当接近耗尽时触发报警,必要时暂停活动。

示例

用户点击抽奖时,系统检查活动库存桶剩余数量。若有库存,则预扣1个并执行抽奖逻辑:中奖则确认扣减,未中奖则回滚。若Redis中库存计数器显示余量为0,则直接返回“奖品已发完”,避免超发。

Image

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

平台持续探索与展望

狐友抽奖平台的构建,不仅解决了初期业务中活动开发效率低、复用性差、运营流程复杂等问题,更通过中台化的设计理念,为未来的业务发展提供了灵活的技术支撑。在平台上线后,我们持续关注用户反馈、技术演进与业务场景的适配性,逐步探索以下方向,以推动平台的长期价值最大化。

4.1 智能化与数据驱动

1.动态策略优化:引入机器学习模型,基于历史活动数据动态调整中奖概率、库存分配规则及用户风控策略,提升活动转化率与资源利用率;
2.用户行为预测:通过大数据分析用户参与习惯,预判活动高峰期并提前进行资源调度,优化系统负载能力;
3.智能客服与反馈闭环:结合AI能力,实现中奖咨询、奖品发放异常等场景的自动化处理,降低人工成本。

4.2 生态化扩展

1.多业务场景支持:开放平台能力,支持跨部门业务接入(如电商促销、内容激励等),通过标准化接口快速适配不同玩法需求;
2.第三方资源整合:与外部合作伙伴共建奖品资源池,支持虚拟权益(如会员卡、优惠券)与实物奖品的无缝对接,提升BD运营效率;
3.社交裂变增强:深化与Feed流、分享组件的联动,设计更多社交化玩法(如组队抽奖、助力解锁),激发用户主动传播。

4.3 运营效率提升

1.零代码配置:通过可视化拖拽界面降低运营门槛,支持复杂规则(如梯度概率、时段限制)的快速配置;
2.自动化测试工具:构建测试用例生成器与Mock数据平台,减少测试人力投入,提升上线效率;
3.效果分析中台:集成BI工具,提供多维度的活动效果分析(如用户留存、ROI计算),辅助运营决策。