活动玩法越堆越乱,我重构了一套事件驱动的活动系统

业务背景

之前我负责一个 C 端项目的时候,用户除了日常花钱打广告买量以外,真正能不能留下来,其实很大一部分还是要靠运营。想提高用户留存,活动功能基本是绕不开的手段。签到、任务、抽奖、新手活动、回归活动,这些在 C 端产品里都非常常见。

我们那个项目当时也是这样,基本上每个大版本都会配一个活动,活动产出非常频繁。但问题在于,项目上线还不到一年,整体架构其实并不成熟,活动这块一开始也没有单独设计,而是跟着业务需求一路"补"出来的。最早的时候,每来一个新活动,基本就意味着要新加接口,或者改一改原有接口逻辑来适配。

后来情况变得更复杂,是因为老板从竞品那边挖来了一个运营大佬。运营思路一下子打开之后,活动形式也开始变得花里胡哨起来:签到、抽奖、邀请、充值活动、新手活动、拉新活动,各种玩法轮着来,需求几乎是隔一段时间就来一波。而我们之前那套"一个活动一套接口"的实现方式,很快就跟不上节奏了,不光扩展成本高,还越来越难维护。

也正是基于这个背景,加上后面活动在项目里会越来越重要,如果继续靠堆接口硬撑下去,迟早会失控,所以不得不重新把活动这块单独拎出来,做一次系统性的重构,拆分成一个独立的活动服务中台,来支撑后续不断增长的活动需求。


设计思路

在决定重构活动系统之后,其实想法并不复杂。 更多还是要从目前的问题出发,先把最头疼的点解决掉。无非就是希望后面活动再多一点,也不要每次都牵扯到大量代码改动。

后来整理需求的时候发现,大部分活动看起来花样很多,但拆开之后,核心逻辑其实并不复杂。大多数活动无非就是几种固定模式的组合。

签到、任务、抽奖,看起来形式很多,但本质上都是围绕「用户行为 → 规则判断 → 发奖励」这条链路在转。真正经常变化的,并不是逻辑本身,而是规则、配置和组合方式。

所以在设计的时候,我给自己定了几个比较明确的方向。

第一点,是活动和玩法一定要拆开

活动本身只关心生命周期,比如什么时候开始、什么时候结束、投放给哪些用户;而真正的业务逻辑,应该全部收敛到玩法里。这样同一个签到玩法、同一个抽奖玩法,就可以被多个活动复用,而不是每个活动都重新实现一遍。

第二点,是玩法必须是可配置的,而不是写死在代码里

不管是签到的天数节点,任务的阶段条件,还是抽奖的保底次数、奖励池,这些都应该由后台配置来驱动,而不是通过 if else 写在代码中。这样运营想怎么组合、怎么调整,基本都不需要再找研发改逻辑。

第三点,是用统一的事件模型来驱动玩法执行

用户登录、签到、充值、邀请,本质上都是行为事件。系统只需要把这些行为统一上报,剩下的事情交给活动系统去判断:哪些活动关心这个事件,哪些玩法需要被触发,规则是否满足,是否需要发奖。

基于这几个思路,整个活动系统就被拆成了几块相对清晰的模块:

活动负责"挂载玩法",玩法负责"规则和奖励",事件负责"驱动执行"。而后台页面,其实就是围绕这几个核心概念,把配置能力暴露给运营使用。

随着系统逐步落地,活动中台的功能也变得越来越完整,但如果把所有实现细节全部展开来讲,反而会显得过重。因此这里我适当做了一些简化,主要围绕整体设计思路来展开说明。

不管是抽奖玩法管理、签到玩法配置,还是任务的阶段规则设计,本质上解决的都是同一件事情:把原本写死在代码里的活动逻辑,拆分成可配置的数据结构,由系统统一驱动执行

后面我会结合实际的后台页面设计以及对应的数据库结构,拆解这些设计思路是如何一步步落到具体实现上的。


玩法体系设计

在活动中台里,玩法是最核心的一层。一个活动能不能吸引用户,本质上还是取决于玩法和奖励的设计。所有活动,最终也都是通过挂载不同的玩法来进行组合的。

这里先从最常见、也最基础的几类玩法说起:签到、抽奖和任务。这三类玩法基本覆盖了大部分常见的活动场景,而且在规则模型和执行方式上都有比较清晰的边界,不太适合强行合成一种"万能玩法"。

签到玩法更偏向时间维度的推进,比如累计签到、连续签到;

抽奖玩法是用户主动触发,需要处理次数限制、概率计算以及保底等规则;

任务玩法则相对灵活,可以根据登录、充值、邀请等不同行为来推进进度。

我在后台设计上,玩法管理被拆成了三个独立的菜单模块,分别是签到玩法、抽奖玩法和任务玩法

每一类玩法都以"玩法模板"的形式进行管理,活动本身并不直接配置规则,而是通过挂载不同的玩法模板来组合出最终的活动效果。

签到玩法

签到玩法是最常见的一类玩法配置,因此在玩法管理中单独作为一个模块进行维护。每一个签到玩法,都是以「玩法模板」的形式存在的,对应后台中的一条签到模板配置。

在签到玩法列表页中,我们可以看到当前系统中已经配置好的多个签到模板。每个模板都会标明签到模式,比如是累计签到还是连续签到,同时也会展示该模板下已经配置好的奖励天数节点。

也就是说,一个签到玩法模板,本身并不是只对应某一天的奖励,而是可以包含多条签到规则。

签到玩法模板与规则设计

以连续签到为例,在编辑某个签到玩法模板的详情时,可以看到该模板下配置了多条签到规则。每一条规则,通常对应一个签到天数节点,例如第 1 天、第 3 天、第 7 天等。

对于每一个签到天数节点,都可以单独配置对应的奖励内容,包括奖励类型、奖励数量等。这些规则之间是相互独立的,可以按需增加或调整,而不需要修改任何代码逻辑。

通过这种方式,一个签到玩法模板就可以完整描述一套签到规则。当后续创建活动时,如果活动需要使用签到玩法,只需要选择一个已经配置好的签到玩法模板,并将其挂载到活动中即可,活动本身不再关心具体的签到规则和奖励细节。

签到玩法在活动中的使用方式

我们在实际使用中,签到玩法模板通常是提前配置好的基础能力。创建活动时,只负责选择需要的签到模板,并控制活动的生效时间和投放范围。

这样做的好处是,玩法和活动实现了解耦

签到规则的调整不会影响活动逻辑,而活动的增加也不会导致签到逻辑重复实现。

抽奖玩法

抽奖玩法在活动中属于比较特殊的一类玩法,它和签到、任务最大的不同点在于:抽奖通常是由用户主动触发的,而不是被某个行为事件自动推进的

在实际的活动设计中,抽奖玩法往往不会单独作为一个"任务"存在,而是和其他玩法进行组合使用。比如完成某些任务后获得抽奖道具,或者在活动期间每天登录可以获得一次抽奖机会,最终由用户在活动页面主动点击进行抽奖。

因此,在抽奖玩法的设计上,系统并不关心抽奖机会是如何获得的,而是只负责定义抽奖本身的规则

抽奖玩法模板与规则配置

在后台中,抽奖玩法同样以「玩法模板」的形式进行管理。抽奖玩法列表页中展示的是当前已经配置好的抽奖模板,包括是否开启保底、保底次数、奖励数量等核心信息。

每一个抽奖玩法模板,代表一套完整的抽奖规则。在编辑抽奖模板时,我们可以对该模板的基础属性和奖励规则进行配置,例如是否启用保底机制、保底触发的次数,以及抽奖按钮的展示文案等。

在奖励配置部分,可以为同一个抽奖模板配置多种奖励项,每个奖励项都可以设置对应的权重和数量,并支持标记为稀有奖励,用于配合保底规则使用。通过这种方式,抽奖玩法可以灵活地支持概率抽取、稀有奖励保底等常见需求。

需要说明的是,实际业务中的抽奖规则往往会更加复杂,比如多层奖池、分阶段保底、动态概率等。这里的示例主要是为了说明抽奖玩法的整体设计思路,因此只做了一个相对简化的 Demo 展示。

抽奖玩法在活动中的使用方式

在活动配置阶段,如果某个活动需要使用抽奖玩法,只需要选择一个已经配置好的抽奖玩法模板,并将其挂载到活动中即可。至于抽奖机会的来源,比如通过任务获得抽奖道具,还是通过其他方式发放,都不属于抽奖玩法本身的职责范围,而是由其他玩法或业务逻辑来完成。

通过这种拆分方式,抽奖玩法只关注"怎么抽"和"抽到什么",而不关心"为什么能抽",从而保持了玩法本身的独立性和可复用性。

任务玩法

随着活动逐渐变多,仅靠签到和抽奖这两类玩法,已经很难支撑后续的运营需求。

在实际运营过程中,常见的活动规则开始变得更加多样化,比如:

  • 登录类行为(每日登录、累计登录)
  • 消费/充值类行为
  • 邀请好友、分享拉新
  • 完成指定任务或行为次数

如果针对每一种行为都单独设计一套玩法,那么后台的玩法类型会迅速膨胀,不仅配置成本高,后续维护也会变得非常混乱。这显然不适合作为一个长期演进的活动中台方案。

基于这个背景,于是我们引入了任务玩法这一层抽象。

为什么要有任务玩法

任务玩法的核心目标,其实是承载多种"行为驱动型"的活动规则

它并不关心活动本身是什么样子,而是关注三个问题:

  1. 用户做了什么行为
  2. 这个行为是否满足某个规则
  3. 是否可以推进进度并发放奖励

相比签到玩法偏向时间维度、抽奖玩法偏向用户主动触发,任务玩法更像是一个事件驱动的规则容器

任务玩法模板列表

在后台设计上,任务玩法同样以"模板"的形式存在,如下图所示:

  • 每一行代表一个任务玩法模板
  • 模板本身有明确的任务标识和任务类型
  • 同一个活动中,可以挂载多个不同的任务玩法模板

这里我们可以看到登录任务、消费任务、邀请任务等都以统一的"任务玩法"形式存在,而不是拆成多个完全不同的玩法模块。

任务玩法的一个关键约束:一个模板只对应一种行为

在设计任务玩法时,有一个非常重要的约定:

每一个任务玩法模板,只绑定一种具体行为

例如:

  • 登录任务模板,只处理登录行为
  • 邀请任务模板,只处理邀请行为
  • 充值任务模板,只处理充值行为

之所以做这样的限制,是因为不同行为下的规则模型和奖励结构差异非常大。 如果允许一个模板同时处理多种行为,那么在规则配置、进度计算和奖励发放上都会变得异常复杂,反而违背了中台"可配置、可扩展"的初衷。

这种设计方式的好处是:

  • 模板本身职责清晰
  • 规则配置逻辑统一
  • 后续新增行为时,只需要新增对应的模板即可

活动层只需要按需挂载多个任务模板,就能组合出非常灵活的活动玩法。

任务玩法的规则与阶段奖励配置

在编辑某一个任务玩法模板时如下图,配置方式整体和签到玩法保持一致:

  • 每个任务可以配置多个阶段
  • 每个阶段有明确的达成条件(次数、天数、金额等)
  • 每个阶段对应一组奖励配置

例如一个登录任务,可以配置:

  • 第 1 天登录奖励
  • 第 3 天登录奖励
  • 第 7 天登录奖励

而一个充值任务,则可能是:

  • 累计充值达到 100
  • 累计充值达到 500

虽然具体规则不同,但在数据结构和配置方式上保持了一致,这样可以让前端、后端和运营侧都使用同一套心智模型来理解任务玩法。

任务玩法在活动中的使用方式

在活动创建时,我们并不需要关心任务玩法的具体规则细节,只需要选择并挂载对应的任务模板即可:

  • 一个活动可以挂载多个任务玩法
  • 每个任务玩法独立记录进度
  • 用户行为通过事件上报驱动任务进度推进

这种设计方式,使得活动本身变成了一个玩法组合器,而不是规则承载者,大大降低了活动配置和迭代的成本。


奖励配置:统一奖励资源的管理层

在前面的玩法配置中,无论是签到、抽奖还是任务,最终都会落到一个结果:发什么奖励、发多少奖励

从逻辑上看,奖励本身并不属于某一个玩法,它只是被玩法规则引用的一种资源。因此在设计时,我们并没有把奖励直接"写死"在各个玩法模板里,而是单独抽了一层出来,作为统一的奖励配置模块。

如上图所示,这个页面主要用于维护系统中所有可用的奖励资源,包括:

  • 各类货币(如金币、钻石)
  • 抽奖券、道具类奖励
  • 礼包、消耗品等复合奖励

每一条奖励都会有明确的类型、唯一标识以及稀有度标记,方便在玩法规则中进行引用和展示。

需要说明的是,这个页面更多是为了后台配置和管理方便而存在。 在实际的数据结构中,玩法规则里引用的只是奖励 ID 或奖励配置快照,底层仍然统一落在同一套奖励配置表中,并不会为每种玩法单独维护奖励数据。

我们这样设计的好处是:

  • 奖励可以在多个玩法、多个活动之间复用
  • 奖励变更不会影响已存在的玩法规则结构
  • 后续扩展新的奖励类型,也不需要改动玩法模型

在此基础上,签到、任务、抽奖等玩法,只需要在各自的规则配置中"引用奖励",而不关心奖励本身的具体实现细节。


页面配置:活动最终如何呈现在用户眼前

前面我们介绍的玩法和奖励,解决的是活动"做什么" 的问题,但对 C 端用户来说,更直观的其实是:活动长什么样、从哪里进入、点了会打开什么页面

因此在活动中台里,还需要一层专门用来管理页面的配置能力,用来描述活动相关的 UI 结构和入口信息。

如上图所示,这里维护的是一组页面模板列表,每一个页面模板,都对应一个可以被活动使用的前端页面资源。

在配置维度上,页面模板主要包含几类信息:

  • 页面名称与唯一标识,用于后台管理和引用
  • 页面角色,用来区分这是主页面、子页面还是弹窗页面
  • 打开方式,例如 App 内页面或外链 H5
  • 页面入口地址,对应实际的前端路由或 URL
  • 状态控制,用于快速启停某个页面模板

从抽象层面看,这里的页面模板并不关心具体的玩法规则,也不直接绑定奖励逻辑,它只负责一件事:定义一个"可被活动使用的页面壳"

页面模板在活动中的作用

在实际的活动配置过程中,活动本身并不会直接写死某一个前端页面,而是通过"挂载页面模板"的方式来完成组合。

比如:

  • 一个活动可以指定一个主页面,作为用户进入活动后的整体展示页
  • 再挂载若干子页面,用于签到、抽奖、任务等具体玩法的交互
  • 某些场景下,还可以额外挂载弹窗页面,用于奖励提示或规则说明

这样做的好处是,页面结构和活动逻辑是解耦的。 同一套页面模板,可以被多个活动复用;而同一个活动,也可以在不改玩法规则的情况下,替换页面样式或入口形式。

实际业务中的使用方式

在我们实际业务中,页面配置往往会比示例中更加复杂,比如:

  • 页面会区分不同客户端版本或渠道
  • 同一个页面模板,可能对应多套前端资源
  • 页面入口可能需要结合灰度、投放策略动态控制

但不管复杂度如何变化,核心思路是一致的: 活动系统只负责"引用页面",而不关心页面内部如何实现。

页面模板作为活动中台的一层基础能力,与玩法模板、奖励配置一起,构成了活动的三个核心拼装单元:

  • 玩法决定活动怎么玩
  • 奖励决定活动给什么
  • 页面决定活动如何展示

当这三层都被配置化之后,活动本身就只剩下"组合与生命周期管理"这一件事了。


活动创建与发布流程设计

到这里为止,玩法模板、奖励配置和页面资源其实都已经准备好了,但这些配置本身并不会直接生效,更像是一组可以反复复用的"素材"。真正让活动跑起来的,是把这些素材按一定规则组合起来,生成一个可以对外投放、可控、可回溯的活动实例。

在活动中台里,创建活动并不是一次性把所有东西都配置完,而是被刻意拆成了几个独立的步骤。整个流程大致分为四步:先创建活动的基础信息,确定这个活动本身的定位;再为活动挂载具体的玩法实例,把已经配置好的玩法模板引入进来;接着绑定活动所需要的页面资源,确定用户实际看到和操作的界面;最后通过发布版本的方式,配置活动的生效时间、投放入口以及灰度策略,让活动真正对外可见。

这样拆分的好处在于,把"活动本体"和"玩法、页面、投放策略"彻底分离开来。活动本身只关心结构和组合关系,而具体怎么玩、怎么展示、什么时候投放,都可以在后续阶段独立调整。这种解耦设计可以显著降低后期修改和反复运营时的风险,也更符合活动高频变更的实际场景。

创建活动的基础信息

如下图所示,活动创建的第一步只负责初始化活动的"壳"。在这个阶段,需要填写的内容主要包括活动名称、所属项目、业务类型以及活动编码等基础信息。

这一阶段并不会涉及任何玩法规则、奖励配置或页面投放相关的内容。其目的在于先在系统中创建一个活动实体,用来承载后续的玩法实例、页面资源和发布版本。可以理解为,先定义"这是一个什么活动",而不是"这个活动怎么玩"。

其中,业务类型更多用于对活动进行分类和管理,比如普通运营活动、节日活动或新手活动等,并不直接影响玩法逻辑。活动编码则作为活动的唯一标识,会在后续的日志、投放、版本管理中被反复使用。

完成这一步后,活动已经具备了一个稳定的基础结构,但仍然是一个"空活动",尚未具备任何实际玩法能力。

挂载玩法实例

在完成活动基础信息的创建后,下一步是为活动挂载具体的玩法实例,如下图所示。

一个活动可以同时包含多个玩法实例,例如签到、抽奖、任务等。这里挂载的并不是临时配置的规则,而是之前已经在玩法管理中配置好的玩法模板。每一个玩法实例,本质上都是对某一个玩法模板的引用。

为了提高运营配置效率,系统支持一键添加常见的玩法组合,也支持按需逐个选择玩法模板进行挂载。无论采用哪种方式,最终都会形成"活动 + 多个玩法实例"的组合结构。

需要注意的是,玩法实例本身并不承载具体的规则细节。具体的阶段规则、奖励内容,仍然由玩法模板统一定义,这样可以保证同一类玩法在不同活动中的行为一致,也便于后期统一调整和复用。

绑定页面资源

当活动已经具备了完整的玩法结构后,接下来需要绑定页面资源,确定活动在客户端中的展示形式,如下图所示。

在这一阶段,可以为活动选择一个或多个页面模板,例如活动主页面、抽奖页面、任务中心页面等。页面同样以模板的形式进行管理,活动只负责引用,而不关心页面内部的具体实现。

这种设计使得页面资源可以被多个活动复用,也方便在不改动活动和玩法配置的情况下,对页面样式或布局进行统一升级。对于运营来说,只需要关注"这个活动需要哪些页面",而不需要重复配置页面逻辑。

发布活动版本

我们最后一步是发布活动版本,这是活动真正对外生效的阶段,如下图所示。

在发布版本中,我们需要配置活动的生效时间、终端类型、投放入口以及投放人群等信息。如果需要进行灰度投放,也是在这一阶段选择对应的灰度策略,而不是直接修改活动本体。

通过引入"版本"的概念,同一个活动可以被多次发布,不同版本之间可以使用不同的投放时间、人群或灰度策略。这种方式可以在不影响活动结构和玩法配置的前提下,支持反复投放和精细化运营,也为回溯历史投放状态提供了基础。

小结

我们通过将活动创建流程拆分为「基础信息 → 玩法实例 → 页面资源 → 发布版本」四个阶段,活动中台把活动的结构定义、玩法能力、页面展示和投放策略彻底解耦。

活动本身只负责描述"由哪些玩法和页面组成",而具体怎么玩、如何展示、什么时候对外投放,则分别由玩法模板、页面模板和发布版本独立控制。这种设计既保证了配置层面的灵活性,也显著降低了活动高频调整和反复运营时的改动成本。


活动列表与版本管理:活动的全生命周期视角

当活动创建并发布之后,真正进入运营阶段的,其实并不是某一个具体的配置页面,而是围绕活动展开的一整套生命周期管理能力。 因此,在活动中台中,活动列表页承担的并不是"简单查看"的角色,而是整个活动管理的入口。

如图所示,我们展示的活动列表会集中展示当前系统中所有活动的核心信息,包括活动名称、所属项目、当前状态、生效时间区间以及创建人等。

通过这些信息,运营可以快速判断一个活动目前处于未发布、运行中、已下线等不同阶段,并对活动整体有一个全局认知。

在列表中,每个活动都提供了两个非常核心的操作入口:配置版本

配置入口:查看活动当前生效结构

点击「配置」后,进入的是活动的配置视图,如下图所示。

这个页面并不是重新创建活动,而是用于查看和调整活动当前所绑定的结构信息,包括:

  • 活动的基础信息(名称、类型、所属项目等)
  • 已挂载的玩法实例及其对应模板
  • 已绑定的页面资源
  • 当前投放和灰度相关配置

可以理解为,这是一个"活动当前形态"的完整快照。 它帮助运营在不进入具体配置步骤的前提下,快速确认:这个活动现在是由哪些玩法组成的?用了哪些页面?当前结构是否符合预期?

版本入口:记录活动的每一次变化

相比配置入口,点击「版本」进入的页面更偏向于活动变更历史的管理,如图所示。

在版本管理页中,可以看到该活动历次发布的版本记录,每一个版本都对应一次明确的投放配置,包括:

  • 绑定的玩法实例集合
  • 使用的页面资源
  • 投放时间范围
  • 灰度策略配置
  • 发布人及发布时间

当前正在运行的版本会被明确标识出来,而历史版本则以只读形式保留,形成一条完整的活动演进链路。

这种设计的核心价值在于: 活动的"结构配置"和"投放行为"是通过版本来承载的,而不是直接覆盖原有活动。

当需要调整活动玩法、页面或灰度策略时,并不会修改当前运行中的版本,而是通过发布一个新版本来完成变更。 这不仅保证了变更过程的安全性,也为问题回溯、灰度验证以及快速回滚提供了基础能力。

活动运行视角:活动不是配置完就结束了

但在真实业务中,活动并不是"发布完成"就算结束。

真正重要的,是活动跑起来之后发生了什么

因此在活动运行模块下,我们通常还会提供几个非常关键的页面。

活动概览:从整体视角看活动效果

活动概览页,关注的是宏观结果

如图所示,这一页通常会展示:

  • 活动整体参与人数
  • 完成人数
  • 领奖人数
  • 不同玩法的参与与完成情况

这些数据并不关心某个具体用户做了什么,而是帮助运营快速判断:

这个活动有没有人来? 玩法是不是被理解了? 转化是不是符合预期?

它解决的是"活动整体跑得怎么样"的问题。

参与用户:把数据落回到用户

而参与用户页面,则是从另一个角度切入------把活动结果落回到具体用户身上

在这个页面中,我们可以看到:

  • 某个用户参与了哪些玩法
  • 当前进度是多少
  • 是否完成
  • 奖励是否领取
  • 具体发生在什么时间

这一层更多服务于问题排查与精细化运营

比如:

  • 为什么某个用户说自己没领到奖励
  • 某个玩法是否存在异常卡进度
  • 某类用户在某个玩法上的流失情况

它不是为了看"数据好不好看",而是为了在真实运营中能把问题查清楚

在实际项目中,需要说明的一点是: 这里展示的「活动概览」和「参与用户」页面,并不是活动运营能力的全部,它们更多是一个偏通用、偏基础的运行视角。

真实业务里,活动往往直接和营收、留存、转化挂钩,运营真正关心的问题通常会更偏向结果,而不是配置本身。

比如在我们的实际项目中,活动运行阶段往往还会进一步关注这些数据:

  • 活动期间带来的 新增用户数、回流用户数
  • 不同玩法对 付费转化率、ARPU 的影响
  • 活动前后 留存、活跃度的变化
  • 不同投放版本、不同灰度策略之间的效果对比
  • 奖励成本与实际产出之间的比例关系

这些数据,通常不会全部塞进活动中台本身,而是会和现有的 数据平台、BI 系统、埋点体系 结合使用。 活动中台更多承担的是 "结构化输出活动行为和结果数据" 的角色,而不是一个完整的数据分析系统。

换句话说:

活动中台解决的是

活动怎么配置、怎么跑、发生了什么

而不是

活动到底赚了多少钱、ROI 怎么算、该不该继续投

这些结论,往往需要结合更高维度的数据来判断。

因此在设计活动系统时,我们通常会刻意把配置与运行能力收敛在中台内 ,而把分析与决策能力开放给外部系统,通过数据打通的方式来完成整体运营闭环。

这种边界划分,反而更有利于系统长期演进,也更贴近大多数真实业务的落地方式。

所以我们把这些页面和功能放在一起看,其实就能很清楚地看到活动中台背后的核心设计思路:

  • 创建与配置阶段,解决的是活动如何被定义、如何被组合
  • 版本与投放阶段,解决的是活动如何被发布、如何被控制生效范围
  • 运行与概览阶段,解决的是活动当前跑得怎么样
  • 用户视角阶段,解决的是问题如何被定位、数据如何被追溯

活动从来都不是一个孤立的配置页面,也不是某一次发布动作,而是一条从配置 → 发布 → 运行 → 观测 → 回溯的完整链路。

也正因为如此,活动中台在设计之初,就不可能只服务某一个角色。 它必须同时兼顾研发的稳定性、运营的灵活性,以及产品对整体效果的可控性。

这也是为什么我们在前面的设计中,会反复强调解耦、分层和版本化------ 因为只有这样,活动系统才能在高频运营和复杂玩法并存的情况下,长期跑得住。


数据库设计:从活动定义到用户行为的完整拆解

在前面的章节中,我们已经从玩法设计、页面配置以及活动创建流程,完整地介绍了活动中台在"配置层"是如何工作的。但这些配置最终都需要一个可靠的数据结构来承载,否则就只能停留在界面层。

那我接下来将结合实际的数据库表结构,从 活动定义 → 玩法绑定 → 页面关联 → 发布版本 → 事件驱动 → 用户数据 这条主线,拆解整个活动中台在数据库层面的设计思路。

这里需要强调的是,这里的数据库设计并不是简单地"为页面建表",而是围绕活动的生命周期来展开的哈。

一、活动定义:活动只是一个组合容器

在数据库层面,我们首先需要一个稳定的"活动本体",用来承载后续所有配置关系。

这个角色对应的就是活动基础信息表。

sql 复制代码
CREATE TABLE `activity_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `activity_name` varchar(128) NOT NULL COMMENT '活动名称',
  `project_code` varchar(64) NOT NULL COMMENT '所属项目',
  `biz_type` varchar(32) NOT NULL COMMENT '业务类型',
  `status` tinyint(4) NOT NULL COMMENT '状态:0未发布 1运行中 2已下线',
  `start_time` datetime DEFAULT NULL COMMENT '活动开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '活动结束时间',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='活动基础信息表';

这张表的设计非常刻意地保持了"克制"。

我们不会在这里放任何玩法、页面、奖励相关的字段,它只解决一件事:系统中存在一个什么活动

可以把它理解为活动的"壳子"。

后续所有配置,都会围绕这个活动 ID 去展开,而不是直接耦合在活动表中。

二、玩法模板:不同玩法,规则模型必须拆开

在玩法设计阶段我们已经提到,签到、抽奖、任务在规则结构和执行方式上差异很大,因此在数据库中并没有强行抽象成一张通用玩法表。

以签到玩法为例,对应的是独立的玩法模板表:

sql 复制代码
CREATE TABLE `activity_signin_play` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `play_code` varchar(64) NOT NULL COMMENT '签到玩法模板编码',
  `name` varchar(128) NOT NULL COMMENT '玩法名称',
  `mode` tinyint(4) NOT NULL COMMENT '签到模式:1累计 2连续',
  `event_action_id` bigint(20) NOT NULL COMMENT '绑定的事件动作',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_play_code` (`play_code`)
) ENGINE=InnoDB COMMENT='签到玩法模板表';

这里的 play_code 是玩法模板的业务唯一标识。

需要注意的是,这一层的玩法模板是全局可复用的,并不直接和某一个活动绑定。

三、玩法规则与奖励:规则描述条件,奖励描述结果

玩法模板只定义"怎么玩",而真正的进度节点和奖励内容,会拆到独立的规则表中。

仍然以签到玩法为例:

sql 复制代码
CREATE TABLE `activity_signin_play_rule` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `signin_play_code` varchar(64) NOT NULL COMMENT '签到玩法模板编码',
  `day_num` int(11) NOT NULL COMMENT '签到天数节点',
  `rewards_json` json NOT NULL COMMENT '奖励配置',
  `sort` int(11) DEFAULT '0',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='签到玩法规则表';

这里我们刻意让规则表只关注两件事:

  • 达成什么条件(第几天、第几个阶段)
  • 达成后发什么奖励

规则和奖励都隶属于玩法模板,而不是活动。

这样做的好处是,同一套玩法规则可以被多个活动复用,避免规则数据被活动污染。

四、玩法实例:活动真正使用的是"实例"

当活动创建完成后,并不会直接引用玩法模板,而是通过玩法实例表,将玩法"挂载"到具体活动中。

sql 复制代码
CREATE TABLE `activity_play_instance` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `activity_id` bigint(20) NOT NULL COMMENT '活动ID',
  `play_template_id` varchar(128) NOT NULL COMMENT '玩法模板编码',
  `instance_code` varchar(128) NOT NULL COMMENT '玩法实例编码(活动内唯一)',
  `status` tinyint(4) DEFAULT '1',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_activity_instance` (`activity_id`,`instance_code`)
) ENGINE=InnoDB COMMENT='活动玩法实例表';

这张表是整个活动中台设计中的一个关键点。

玩法模板是"蓝图",而玩法实例是模板在某个活动中的一次具体使用

活动只和实例发生关系,而不是直接依赖模板,这为后续扩展和隔离提供了空间。

五、页面关联:活动只关心"用哪些页面"

页面在数据库中同样被当成一种可复用资源来管理。

活动并不关心页面内部如何实现,只关心使用了哪些页面。

sql 复制代码
CREATE TABLE `activity_page_bind` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `activity_id` bigint(20) NOT NULL COMMENT '活动ID',
  `page_code` varchar(64) NOT NULL COMMENT '页面模板编码',
  `sort` int(11) DEFAULT '0',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='活动页面绑定表';

通过这种方式,页面模板可以在多个活动中复用,活动本身只维护引用关系。

六、发布版本:对外生效的最小单元

在真实业务中,活动并不是"改完就生效",而是通过发布版本来对外投放。

sql 复制代码
CREATE TABLE `activity_release` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `activity_id` bigint(20) NOT NULL COMMENT '活动ID',
  `version_code` varchar(64) NOT NULL COMMENT '版本号',
  `status` tinyint(4) NOT NULL COMMENT '状态',
  `start_time` datetime NOT NULL,
  `end_time` datetime DEFAULT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='活动发布版本表';

版本的引入,使得活动具备了可回滚、可审计、可多次投放的能力,而不是简单地覆盖配置。

七、事件体系:统一用户行为的输入层

在活动进入运行阶段之前,我们需要先解决一个问题:
系统里发生的各种用户行为,应该如何被统一描述和接收。

为此,我们在数据库层面单独抽象了一套事件体系,用来对用户行为做标准化建模。

这一层的目标并不是处理玩法逻辑,而是把"用户做了什么"转换成一组可被玩法消费的标准事件

事件类型:行为的宏观分类

事件类型用于对用户行为进行第一层归类,例如签到类、任务类、系统类等。

sql 复制代码
CREATE TABLE `activity_event_type` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `type_key` varchar(64) NOT NULL COMMENT '事件类型唯一key',
  `type_name` varchar(128) NOT NULL COMMENT '事件类型名称',
  `description` varchar(255) DEFAULT NULL COMMENT '事件说明',
  `status` tinyint(4) DEFAULT '1' COMMENT '1启用 0停用',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_type_key` (`type_key`)
) ENGINE=InnoDB COMMENT='活动事件类型表';

这一层并不关心具体发生了什么行为,它更多是为了逻辑分组和语义表达,方便在代码和配置中进行统一管理。

事件动作:可被玩法消费的最小行为单元

真正参与玩法推进的,是事件动作。

sql 复制代码
CREATE TABLE `activity_event_action` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `action_key` varchar(64) NOT NULL COMMENT '事件动作唯一key',
  `action_name` varchar(128) NOT NULL COMMENT '动作名称',
  `event_type_id` bigint(20) NOT NULL COMMENT '所属事件类型',
  `description` varchar(255) DEFAULT NULL COMMENT '触发说明',
  `params_schema` json DEFAULT NULL COMMENT '事件参数结构定义',
  `status` tinyint(4) DEFAULT '1',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_action_key` (`action_key`),
  KEY `idx_event_type_id` (`event_type_id`)
) ENGINE=InnoDB COMMENT='活动事件动作表';

事件动作代表的是一个可以被系统识别并上报的具体行为,例如登录成功、完成一次签到、完成一次充值等。

在运行时,业务系统只需要上报 action_key,而不需要关心这个行为最终会驱动哪种玩法。这种设计使得事件体系可以长期稳定存在,而玩法逻辑可以持续演进。

在这套模型中,有一个明确的边界约定:
事件本身不关心活动和玩法,玩法主动去匹配和消费事件。

事件体系解决的是"发生了什么",而不是"应该给用户什么结果"。

八、用户行为与奖励:事件驱动后的结果落库

当事件被上报并匹配到对应的玩法规则后,最终都会转化为用户维度的数据变化。这些变化需要被可靠地记录下来,才能支撑进度计算、奖励发放以及后续的数据统计。

这一层对应的,就是用户玩法进度表和用户奖励表。

sql 复制代码
CREATE TABLE `activity_user_play` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `activity_id` bigint(20) NOT NULL,
  `event_type_id` bigint(20) NOT NULL,
  `event_action_id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL,
  `progress` int(11) DEFAULT '0',
  `status` tinyint(4) DEFAULT '0',
  `last_progress_date` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='用户活动玩法进度表';
sql 复制代码
CREATE TABLE `activity_user_reward` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `activity_id` bigint(20) NOT NULL,
  `event_action_id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL,
  `reward_key` varchar(64) NOT NULL,
  `reward_json` json NOT NULL,
  `status` tinyint(4) DEFAULT '0',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='用户活动奖励表';

这两张表记录的不是规则,也不是配置,而是已经发生的事实

  • 某个用户在某个活动中,某个行为推进到了什么进度
  • 某个奖励是否已经发放、是否已经领取

它们是整个活动系统中最底层、也是最关键的一层数据,所有配置最终都会在这里体现为真实的运行结果。


事件驱动上报:活动系统是如何"跑起来"的

在前面的章节中,我们已经介绍了玩法、活动、事件和用户数据在数据库层面的设计。但这些设计最终能不能成立,关键还在于:一次用户行为,是如何被系统接住并正确处理的

这一节,我们就从事件驱动的角度,串一下整个活动系统在运行阶段的核心处理链路。

一、整体处理流程

在活动系统中,一次事件从发生到最终发放奖励,大致会经过下面这条链路:

arduino 复制代码
业务行为发生
  ↓
上报 action_key
  ↓
查询事件定义(action → type)
  ↓
按玩法类型分发(Task / Signin / Lottery)
  ↓
查询对应玩法模板
  ↓
查询哪些活动绑定了该模板
  ↓
推进玩家进度
  ↓
判断规则是否达成
  ↓
发放奖励(幂等)

我们可以看到,整个过程是严格分层的

业务系统只负责上报行为,活动系统负责解释行为、匹配玩法,并把结果落到用户数据中。

二、事件上报:只上报「发生了什么」

事件驱动的第一步,是业务系统上报事件。

在我们的设计中,这一步被刻意做得非常"克制",业务系统只需要告诉活动系统:发生了什么行为,而不需要关心任何活动、玩法或奖励相关的逻辑。

伪代码如下:

java 复制代码
reportEvent("login.success", userId);

这里的 action_key 是行为的唯一标识,比如登录成功、完成一次签到、完成一次充值等。

这一层的核心原则是:
业务系统只关心行为本身,不关心这个行为会触发什么活动。

三、第一步:根据 action_key 找到事件定义(关键转折点)

事件上报进入活动系统后,首先要做的一件事,是把这个行为"解释清楚"。

伪代码如下:

java 复制代码
EventAction action = findEventAction(actionKey);
// action.actionKey = login.success
// action.typeKey   = TASK

通过 action_key 查询事件定义,我们可以得到两个关键信息:

  • 这是一个什么具体行为(action)
  • 这个行为属于哪一类玩法域(type)

这一步的意义非常重要。

它把"一个具体行为",映射到了"一个玩法领域",例如任务类、签到类或抽奖类。

也正是从这里开始,事件不再是一个简单字符串,而是进入了活动系统的语义世界。

四、第二步:按 type_key 分发到不同玩法域

拿到事件类型之后,下一步就是进行玩法域分发

伪代码如下:

java 复制代码
switch (action.typeKey) {
    case TASK:
        handleTaskEvent(action);
        break;
    case SIGNIN:
        handleSigninEvent(action);
        break;
    case LOTTERY:
        handleLotteryEvent(action);
        break;
}

这一层的分发非常关键。

不同玩法的数据模型、规则结构和执行方式完全不同,如果把所有逻辑揉在一起,后续代码一定会变得混乱且不可维护。

因此,我们选择在事件入口处就进行一次明确的"玩法域切分",后面的处理逻辑各自独立,互不干扰。

五、第三步:在玩法域内,找到对应的玩法模板

进入具体玩法域之后,下一步是确定:这个行为对应的是哪一个玩法模板

以任务玩法为例,伪代码如下:

java 复制代码
TaskPlay play = findTaskPlayByAction(action.id);

例如 login.success 这个行为,就会命中"登录任务"这一玩法模板。

在我们的设计中,有一个非常明确的约定:

一个行为,对应一个玩法模板。

玩法模板内部,会定义该行为的规则结构,例如阶段条件、目标值和奖励配置。

这一层我们只做匹配,不展开规则细节。

六、第四步:找到哪些「进行中的活动」绑定了该模板

接下来,是活动系统和玩法系统真正交汇的地方。

伪代码如下:

java 复制代码
List<Activity> activities =
    findRunningActivitiesByPlay(play.id);

这一步要解决的问题是:
当前有哪些正在运行的活动,使用了这个玩法模板。

因为同一个玩法模板是可以被多个活动复用的,所以事件触发时,必须在运行中的活动范围内进行筛选,而不是直接绑定到某一个活动。

这一步保证了玩法的复用能力,也保证了活动生命周期的正确性。

七、第五步:推进玩家进度(状态驱动)

确定了活动和玩法之后,系统会加载该玩家在对应活动下的玩法进度。

伪代码如下:

java 复制代码
UserPlayProgress progress =
    loadUserProgress(activity, play, userId);

progress.advanceIfNeeded(event);

这里的设计核心是:
玩家进度是按「活动 + 玩法 + 用户」维度维护的。

即使同一个事件被重复上报,也只会在状态允许的情况下推进一次进度,从而避免重复累计。

八、第六步:判断规则是否达成,发放奖励

当进度发生变化后,系统会判断是否命中了某个规则节点。

伪代码如下:

java 复制代码
if (progress.reachedRule(rule)) {
    grantRewardOnce(activity, play, rule, userId);
}

奖励发放并不是简单地"判断条件就发",而是基于规则节点进行,并通过唯一约束或幂等控制,确保同一个奖励只会被发放一次。

即使事件被重复处理,最终也不会出现重复发奖的问题。

九、事件到奖励的完整处理路径

回顾整个事件驱动的处理流程,我们就可以发现活动系统在运行阶段,始终围绕一条非常清晰的主线在工作:

用户行为先被抽象为事件,再由玩法消费事件,最后在活动维度下结算规则并发放奖励。

从系统职责划分上看:

  • 业务系统只负责上报"发生了什么行为"
  • 事件体系负责对行为进行标准化和分类
  • 玩法模块负责解释行为并推进规则进度
  • 活动系统负责限定生效范围和生命周期
  • 奖励模块负责最终结果的幂等落库

也正是通过这种分层设计,用户行为、玩法规则和活动配置之间才能保持解耦。

新增行为时,不需要侵入活动逻辑;新增玩法时,也不需要改动业务代码。

为了更直观地理解这条链路在系统中的实际执行过程,我们下面用一张流程图,把一次事件从发生到最终发放奖励的整体路径串联起来。

我们可以看到,签到和任务这类被动行为统一走事件驱动链路,而抽奖作为一种主动玩法,虽然执行路径不同,但最终仍会以事件的形式回流到活动系统中,用于统计、联动或后续规则判定,形成一个完整闭环。

正是通过这条事件驱动链路,活动中台才能在不侵入业务代码的前提下,支撑复杂玩法组合和高频运营调整。


关于活动中台落地的一些补充说明

到这里为止,这套活动系统在设计层面其实已经是完整的了。无论是事件驱动、玩法拆分,还是活动和奖励的组合方式,从结构上看都已经可以覆盖大多数常见场景。

但放到真实业务里,会发现活动系统往往比设计阶段要复杂得多,也更"厚重"一些。运营节奏快、需求变化频繁,很多规则并不是一开始就能想清楚,而是在不断上线、调整和复盘的过程中逐步演化出来的。

比如在实际运行中,事件重复上报几乎是必然会发生的事情。网络重试、客户端异常、异步消费失败,都会导致同一个行为被多次处理。如果在设计阶段没有把幂等控制放到用户数据这一层,后面很容易出现进度错乱或者重复发奖的问题。

另外,活动一旦上线,就很难再"完全推倒重来"。玩法配置和页面资源可以调整,但已经产生的用户进度和奖励,本质上是一种已经发生的事实,只能在此基础上向前兼容,而不能简单覆盖或重置。这也是为什么在活动系统里,会刻意区分"配置数据"和"运行数据",避免两者相互影响。

抽奖玩法在实际业务中也往往比预期复杂。除了概率和保底规则,还会涉及抽奖道具来源、每日次数限制、风控校验等问题。这类逻辑更接近交易行为,而不是单纯的状态推进,因此在设计时会选择让抽奖走一条相对独立的执行路径,而不是强行并入通用事件链路。

还有一个很现实的情况是,运营配置几乎一定会发生临时调整。比如活动延长、奖励内容修改、投放范围变更等,这就要求活动的发布和投放策略具备版本化和可回溯能力,而不是简单地"改一条配置就生效"。

也正是基于这些实际情况,这套活动系统在设计时才会刻意把行为、玩法和活动拆得比较松。很多复杂度并不是一次性解决的,而是通过清晰的边界,把变化控制在局部范围内,让系统在高频活动和持续迭代的情况下,仍然有足够的弹性继续演进。


结语

我们在把项目的活动系统进行拆分重构后,就如上文设计的那样,文中展示的玩法拆分、事件驱动和活动组合方式,本质上是一种思路层面的整理。它解决的并不是"怎么把所有活动一次性设计完",而是试图回答一个更现实的问题:当活动不断叠加、规则不断变化时,系统还能不能保持可控。

需要说明的是,我这里的设计并不是放之四海而皆准的标准答案。不同项目的业务形态、用户规模、运营节奏都不一样,实际落地时一定需要结合自身情况进行取舍和调整。有些项目可能不需要这么细的玩法拆分,有些项目可能会在事件层引入更多复杂规则,这些都很正常。

所以这套设计更像是一次重构过程中的阶段性结果,而不是终点。随着业务发展,玩法会继续变多,规则会继续变复杂,系统本身也需要不断演进。至少在当前阶段,这样的结构能让活动系统在高频运营和持续迭代的情况下,保持相对清晰的边界,而不至于失控。

相关推荐
Mintopia3 小时前
🏗️ React 应用的主题化 CSS 架构方案
前端·react.js·架构
章豪Mrrey nical3 小时前
数组扁平化的详解
开发语言·前端·javascript·面试
柯杰3 小时前
DNS劫持防护:从被动监测到主动防御
后端·dns
墨守城规3 小时前
CompletableFuture 使用与分析
后端
爱叫啥叫啥4 小时前
你都知道哪些嵌入式中的常用关键字
后端
a程序小傲4 小时前
淘宝Java面试被问:Atomic原子类的实现原理
java·开发语言·后端·面试
expect7g4 小时前
Paimon源码解读 -- Compaction-9.SortMergeReaderWithLoserTree
大数据·后端·flink
程序员爱钓鱼4 小时前
BlackHole 2ch:macOS无杂音录屏与系统音频采集完整技术指南
前端·后端·设计模式
与遨游于天地4 小时前
接口与实现分离:从 SPI 到 OSGi、SOFAArk的模块化演进
开发语言·后端·架构