写一个按概率返回奖品的方法并不复杂。给每个奖品分配一段区间,生成随机数,找到落在哪个区间就可以了。
但加上参与次数、中奖上限、库存和不同场次以后,问题就不只是随机数了。用户还有一次机会,却已经达到中奖上限,这次请求怎么处理?两个场次使用同一种奖品,库存是共用还是各算各的?奖品没了,原来的概率分给其他奖品,还是按未中奖处理?
这些问题会直接影响模型和执行流程。先把含义定下来,后面写规则、扣库存和记录结果时,才有共同的依据。
下面用一个简化的例子讨论:一次抽奖活动分成多个场次,每个场次可以配置自己的奖品和概率,用户同时受活动总次数和场次次数限制。例子中的规则和数值仅用于说明设计。
先分清参与和中奖
假设每个用户在活动期间可以参与五次,但最多中奖一次。
用户第一次中奖了,第二次请求还有参与机会。这里至少有两种定义:达到中奖上限以后直接禁止参与,或者允许继续抽,但不会再中奖。
两种做法都能实现,含义却不同。前者改变了参与资格,后者只限制中奖。如果所有判断都放进一个 canDraw(),返回一个 false,调用方很难区分到底是次数用完了,还是不能再中奖。
这里把两类条件分别表达:
| 判断 | 关心的问题 | 不满足时的结果 |
|---|---|---|
| 参与条件 | 时间是否允许、是否有参与资格、次数是否充足 | 未参与 |
| 中奖条件 | 是否达到中奖次数或额度上限 | 已参与,但未中奖 |
这里选择“有参与机会就允许抽,中奖限制单独判断”的语义。一次有效参与即使没有中奖,也会消耗机会。
这个区别还会影响结果查询。只返回奖品 ID,空值既可能表示没资格,也可能表示正常未中奖,还可能是系统执行失败。页面和重试逻辑都要继续猜。
结果可以先分为三个确定的业务状态:未参与、已参与未中奖、已中奖。执行中的异常另外处理,尤其是提交结果不确定时,不能直接当作普通未中奖,再放开一次新的抽奖。
“抽到了什么”和“这次请求有没有形成一次参与”,需要分别有答案。
场次、奖品和投放关系
活动可以承载总次数、整体有效期等公共限制,场次承载局部规则。例如总共允许参与五次,其中某个场次最多两次。只有全部限制都满足,才允许参与。
场次也可以决定本次使用哪些奖品,但不宜把概率直接写在奖品定义上。
同一个奖品在场次 A 中概率为 10%,在场次 B 中概率为 5%,奖品本身没有变化,变化的是它在这次抽奖中的投放方式。
每个场次可以有多个投放项。下图只画出其中一个,两个投放项引用同一个奖品定义,各自配置概率;库存约束也由投放项关联,可以共享或独立。
奖品定义描述奖品是什么;投放项描述这个奖品在某个场次中怎么参与抽取。名称、展示信息发生变化,不需要跟着复制所有概率规则;调整某个场次的概率,也不会影响另一个场次。
这里划分的是配置和逻辑的归属,可以在同一个应用里实现。一次抽奖读取当前场次的投放项,再结合活动级限制执行,不必加载其他场次的全部奖品。
库存的单位要明确
库存不一定都按“一次减一”计算。
假设一个奖品每次发放一份,库存限制的是份数;另一个奖品每次发放不同数量的积分,限制的则可能是积分总额。二者都可以检查剩余量,但一次中奖消耗多少,计算方式不同。
可以把库存约束写成:
已使用量 + 本次消耗量 <= 总量份数库存的本次消耗量为 1;积分额度的本次消耗量为这次确定发放的积分数。数值相加之前,单位必须相同,不能把中奖次数和积分数混在一个累计值里。
奖品与库存也不必一一对应。两个投放项可以使用同一个库存池,也可以各自分配库存。这取决于希望它们共享总量,还是相互隔离。
这样拆开以后,增加一个场次不会天然增加一份库存。配置引用同一个库存池,两个场次就共同消耗这一份额度;引用不同的库存池,才分别计数。
例如每份奖品发放 20 积分,可以同时限制最多发放 100 份、与其他奖品共用的积分预算最多消耗 5000。一次中奖分别消耗一份和 20 积分,两项约束都要满足。
如果同一个库存池只是同时被活动和投放项引用,则仍然是同一份资源。本次消耗要按实际资源归并,不能因为配置中引用了两次,就扣减两次。
至于这些占用怎样一起成功或失败,属于后面的提交过程。模型需要先表达清楚本次中奖到底消耗哪些资源、各消耗多少。
规则判断与状态修改分开
规则越多,越容易出现一种写法:检查参与次数的方法顺便增加次数,检查库存的方法顺便减库存,后面的规则不通过时再尝试补回来。
这样规则就很难组合。换一下顺序,或者为了展示剩余次数调用一次检查方法,都可能改变状态。
规则判断可以尽量只读取输入,返回是否通过和原因。需要的数据提前组织成一次执行的上下文,例如当前时间、用户累计次数、中奖情况和当前配置。
图中只保留主要职责顺序,省略未参与、未中奖和异常分支。它不表示这些步骤已经具备并发安全性。前面的检查全部通过,也不能证明最后一定能提交成功。读取状态以后,别的请求可能已经用掉最后一份库存。
因此要区分两件事:规则判断说明“根据目前读到的数据,这个结果是否允许”;提交操作负责保证“真正修改状态时,约束仍然成立”。
如果要求每人最多中奖一次,两个请求都读到中奖次数为零,两个判断都可能通过。把判断方法抽成策略类,不会自动消除这个竞争。真正的限制需要在状态更新处得到保证,不能只依赖预检查。
规则的顺序什么时候有影响
如果几个规则读取同一份输入、不修改状态,也不依赖彼此的计算结果,并且要求全部通过,那么交换判断顺序不会改变最终是否通过的结论。
但返回的拒绝原因可能不同。用户既超过次数,又不在有效期内,哪个原因优先展示,需要固定下来。否则同样的请求可能因为规则加载顺序不同,得到不同提示。
还有一些规则存在数据依赖,顺序就不能随意调整。
例如概率按照“第几次参与”配置。已经参与了两次,当前请求是第三次,概率计算应该读取 3,而不是已完成次数 2。可以在本次上下文里明确计算:
本次参与序号 = 已完成参与次数 + 1这个计算说明的是序号的含义,不代表简单加一就能分配唯一序号。同一用户同时发起两次请求时,可能都算出 3。如果概率依赖参与序号,还需要协调这两次执行,避免它们都按第三次参与计算。
再比如累计最多获得 100 积分,当前已经获得 90 积分,本次候选奖品为 20 积分。抽奖之前检查“90 小于 100”只能说明还有额度,确定奖品以后仍需检查“90 + 20 是否超过 100”。
前置检查可以提前排除必然不满足的情况,结果检查则处理依赖本次奖品的信息。不能因为抽奖前查过一次额度,就省掉后面的计算。
筛选奖品会改变概率含义
假设配置为:
| 结果 | 概率 |
|---|---|
| 奖品 A | 10% |
| 奖品 B | 20% |
| 未中奖 | 70% |
现在 A 没有库存了,能不能直接把 A 从候选集合移除?
要看概率算法怎么解释剩下的数值。
这里采用保留剩余奖品概率的方式:过滤 A 后,B 的概率仍然为 20%,不足 100% 的部分补为未中奖,因此未中奖概率变为 80%。具体实现可以重新构造抽取区间,不要求保留 A 原来的区间位置。
作为对照,另一种方式是删除 A 后,按剩余权重重新归一化。如果 B 和未中奖仍保留权重 20、70,B 的概率就变成:
20 / (20 + 70) ≈ 22.22%两种方式都保留了未中奖结果,区别在于 A 原来占用的概率怎么处理:固定区间把它转为未中奖,重新归一化则按比例分给剩余结果。
如果配置要求 B 的抽取概率始终为 20%,这里就应保留固定区间。如果配置表达的是各结果之间的相对权重,则可以归一化,但配置页面和算法需要使用同一种含义。固定概率的总和不能超过 100%,不足的部分可以约定为未中奖。
抽中 A 后才发现库存不足,也会遇到同一个问题。如果重新抽取,B 就获得了额外的被选中机会;如果按未中奖结束,B 的原有区间不变。因此“缺货后重抽”会改变抽取规则,需要与概率策略一起定义。
这里讨论的是抽取阶段的概率。最终成功记录的中奖比例还会受库存竞争等因素影响,不能只拿配置值作为最终发放比例。
哪些内容适合配置
参与次数、有效时间、概率数值、额度上限,这些通常可以通过配置调整。规则执行顺序、状态提交方式、异常后是否重试,则会改变程序行为,不宜只当成几个普通开关。
例如把“最多中奖三次”改成“五次”,是同一种规则的参数变化;从“按固定概率抽取”改成“保证每个用户在一定次数内中奖”,已经改变了算法及其依赖的状态,不只是增加一个概率值。
实现时可以先保留固定的执行流程,在明确的变化位置使用策略。参与限制负责判断参与条件,概率策略负责计算抽取分布,库存策略负责计算消耗。流程本身负责调用顺序以及结果如何提交。
这样增加一种概率算法时,不需要顺便改动参与次数的记录方式,也不会让概率计算代码直接操作发奖接口。
配置还需要保持一次请求内的一致性。假设请求开始时读到一组奖品,中途概率被修改,后面又读取了新概率,最终使用的组合可能从未作为一份完整配置存在过。
如果要支持运行中修改配置,可以进一步引入配置版本,在请求开始时取得同一版本的有效配置,执行期间复用。这里固定的是奖品和规则等配置,并不意味着库存也被冻结;库存仍然需要在提交时检查实时约束。例如奖品列表和概率分别加载时,可以用共同的配置版本关联,组合完成后再供请求使用,避免读到新旧混合的一组配置。
抽奖结果与发奖结果
抽奖决定的是用户获得什么,发奖负责把这个结果兑现。奖品已经确定并保存,后面的发放调用超时,不应该因此再随机一次。
否则一次外部调用失败,就可能让同一次参与产生两个不同的奖品结果。重试时无法判断应该补发哪个,参与次数和库存也很难对应。
因此可以把确定的抽奖结果作为后续发放的依据,让发放状态单独变化。重试发放时,继续使用原来的结果;重复查询时,也返回已经确定的结果。
文章前面区分了未参与、未中奖和中奖,到这里还要补上一层:已中奖不等于已发放。这个区别提前留在模型里,后面接入不同类型的奖品时,就不需要重新解释中奖记录的含义。
后面讨论并发控制时,需要围绕这些状态继续展开:一次参与怎样避免重复扣次数,中奖结果怎样与资源消耗一起确定,发放失败怎样沿用原结果重试。


