|

Aimee

Write the Code. Change the World.

接到大转盘需求,先想清楚奖品怎么"发完就没了"

· 分享镜

接到大转盘需求,先想清楚奖品怎么"发完就没了"

大转盘、刮刮乐这类活动,需求描述通常是:iPhone 概率 1%,优惠券概率 99%。

看起来很简单,配个概率表就行。但发上线才发现:iPhone 只备了 10 台,活动跑了一天,发出去 47 台。


只配概率,控不住总量

概率决定的是频率,不是总量——这个问题在集卡文章里也提过。

1% 的概率 × 5000 次抽奖 = 期望 50 台。备货只有 10 台,剩下 40 台发的是什么?发的是系统 bug。

正确的设计是:奖品池 + 兜底奖


奖品池:提前把奖品放进去

活动开始前,把所有实物奖品预写进 Redis List:

# 活动初始化:10 台 iPhone 入池
for i in range(10):
    redis.rpush("prize_pool:iphone", f"sn_{i:04d}")

抽奖时,先用概率算"这次应该中什么奖",再去池子里取:

def draw(user_id: str) -> str:
    prize_type = weighted_random(prize_config)  # 按概率选奖品类型

    if prize_type == "iphone":
        sn = redis.rpop("prize_pool:iphone")    # 原子取一个
        if sn:
            record_prize(user_id, "iphone", sn)
            return f"恭喜获得 iPhone!序列号 {sn}"
        else:
            # 池子空了,降级到兜底奖
            prize_type = "coupon"

    coupon = issue_coupon(user_id)
    record_prize(user_id, "coupon", coupon)
    return "获得 5 元优惠券"

RPOP 是原子操作,并发下不会重复发同一个序列号。池子空了就降级,不会超发。


兜底奖:实物送完之后发什么

这个问题很容易忘记设计。

常见方案是配一个"保底奖"(积分、小面额优惠券),实物奖品池取空后自动切换过去。用户看到的转盘结果可以是"非常遗憾,iPhone 已被抢完,送你 10 积分安慰一下",也可以不提实物售罄,直接出优惠券结果。具体看产品决策。

兜底奖本身通常不需要放进池子(数量不限),配置一个固定发放逻辑就行。


奖品概率要动态调整吗

活动前期和后期,奖品剩余数量差异很大,是否需要根据剩余量动态调整概率?

一般不需要——实物券池子控总量,概率只控制"命中实物奖品入口"的频率,命中了但取不到就降级,效果一样。强行动态调整概率反而会让概率计算变复杂,而且用户感知不到。

有一个例外:活动快结束时,剩余实物奖品很少,需要人工下线奖品类型(把 iPhone 从概率表中删掉),防止用户看到 iPhone 但永远取不到。这是运营操作,代码上留个开关就行。


发奖记录必须幂等

转盘动画和发奖是异步的,网络抖动下用户可能多次触发抽奖接口。

每次抽奖生成一个 requestId,发奖记录表用 requestId 做唯一索引:

CREATE TABLE prize_record (
    request_id  VARCHAR(64) PRIMARY KEY,
    user_id     BIGINT,
    prize_type  VARCHAR(32),
    prize_value VARCHAR(64),
    created_at  DATETIME
);

接口进来先查记录,有就直接返回上次结果,不重复发奖。


上线前必须确认的两件事

  1. 奖品池已初始化:入池操作在活动开始前跑,不要等活动开始时再初始化,那时流量最大,初始化失败概率高。
  2. 兜底奖已配置且可用:测试兜底路径,确认池子空了还能正常发奖,不要等活动现场发现是空白页。

评论0

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部