业务背景
之前我负责一个 C 端项目的时候,用户除了日常花钱打广告买量以外,真正能不能留下来,其实很大一部分还是要靠运营。想提高用户留存,活动功能基本是绕不开的手段。签到、任务、抽奖、新手活动、回归活动,这些在 C 端产品里都非常常见。
我们那个项目当时也是这样,基本上每个大版本都会配一个活动,活动产出非常频繁。但问题在于,项目上线还不到一年,整体架构其实并不成熟,活动这块一开始也没有单独设计,而是跟着业务需求一路"补"出来的。最早的时候,每来一个新活动,基本就意味着要新加接口,或者改一改原有接口逻辑来适配。
后来情况变得更复杂,是因为老板从竞品那边挖来了一个运营大佬。运营思路一下子打开之后,活动形式也开始变得花里胡哨起来:签到、抽奖、邀请、充值活动、新手活动、拉新活动,各种玩法轮着来,需求几乎是隔一段时间就来一波。而我们之前那套"一个活动一套接口"的实现方式,很快就跟不上节奏了,不光扩展成本高,还越来越难维护。
也正是基于这个背景,加上后面活动在项目里会越来越重要,如果继续靠堆接口硬撑下去,迟早会失控,所以不得不重新把活动这块单独拎出来,做一次系统性的重构,拆分成一个独立的活动服务中台,来支撑后续不断增长的活动需求。
设计思路
在决定重构活动系统之后,其实想法并不复杂。 更多还是要从目前的问题出发,先把最头疼的点解决掉。无非就是希望后面活动再多一点,也不要每次都牵扯到大量代码改动。
后来整理需求的时候发现,大部分活动看起来花样很多,但拆开之后,核心逻辑其实并不复杂。大多数活动无非就是几种固定模式的组合。
签到、任务、抽奖,看起来形式很多,但本质上都是围绕「用户行为 → 规则判断 → 发奖励」这条链路在转。真正经常变化的,并不是逻辑本身,而是规则、配置和组合方式。
所以在设计的时候,我给自己定了几个比较明确的方向。
第一点,是活动和玩法一定要拆开 。
活动本身只关心生命周期,比如什么时候开始、什么时候结束、投放给哪些用户;而真正的业务逻辑,应该全部收敛到玩法里。这样同一个签到玩法、同一个抽奖玩法,就可以被多个活动复用,而不是每个活动都重新实现一遍。
第二点,是玩法必须是可配置的,而不是写死在代码里 。
不管是签到的天数节点,任务的阶段条件,还是抽奖的保底次数、奖励池,这些都应该由后台配置来驱动,而不是通过 if else 写在代码中。这样运营想怎么组合、怎么调整,基本都不需要再找研发改逻辑。
第三点,是用统一的事件模型来驱动玩法执行 。
用户登录、签到、充值、邀请,本质上都是行为事件。系统只需要把这些行为统一上报,剩下的事情交给活动系统去判断:哪些活动关心这个事件,哪些玩法需要被触发,规则是否满足,是否需要发奖。
基于这几个思路,整个活动系统就被拆成了几块相对清晰的模块:
活动负责"挂载玩法",玩法负责"规则和奖励",事件负责"驱动执行"。而后台页面,其实就是围绕这几个核心概念,把配置能力暴露给运营使用。
随着系统逐步落地,活动中台的功能也变得越来越完整,但如果把所有实现细节全部展开来讲,反而会显得过重。因此这里我适当做了一些简化,主要围绕整体设计思路来展开说明。
不管是抽奖玩法管理、签到玩法配置,还是任务的阶段规则设计,本质上解决的都是同一件事情:把原本写死在代码里的活动逻辑,拆分成可配置的数据结构,由系统统一驱动执行。
后面我会结合实际的后台页面设计以及对应的数据库结构,拆解这些设计思路是如何一步步落到具体实现上的。
玩法体系设计
在活动中台里,玩法是最核心的一层。一个活动能不能吸引用户,本质上还是取决于玩法和奖励的设计。所有活动,最终也都是通过挂载不同的玩法来进行组合的。
这里先从最常见、也最基础的几类玩法说起:签到、抽奖和任务。这三类玩法基本覆盖了大部分常见的活动场景,而且在规则模型和执行方式上都有比较清晰的边界,不太适合强行合成一种"万能玩法"。
签到玩法更偏向时间维度的推进,比如累计签到、连续签到;
抽奖玩法是用户主动触发,需要处理次数限制、概率计算以及保底等规则;
任务玩法则相对灵活,可以根据登录、充值、邀请等不同行为来推进进度。
我在后台设计上,玩法管理被拆成了三个独立的菜单模块,分别是签到玩法、抽奖玩法和任务玩法。

每一类玩法都以"玩法模板"的形式进行管理,活动本身并不直接配置规则,而是通过挂载不同的玩法模板来组合出最终的活动效果。
签到玩法
签到玩法是最常见的一类玩法配置,因此在玩法管理中单独作为一个模块进行维护。每一个签到玩法,都是以「玩法模板」的形式存在的,对应后台中的一条签到模板配置。

在签到玩法列表页中,我们可以看到当前系统中已经配置好的多个签到模板。每个模板都会标明签到模式,比如是累计签到还是连续签到,同时也会展示该模板下已经配置好的奖励天数节点。
也就是说,一个签到玩法模板,本身并不是只对应某一天的奖励,而是可以包含多条签到规则。
签到玩法模板与规则设计
以连续签到为例,在编辑某个签到玩法模板的详情时,可以看到该模板下配置了多条签到规则。每一条规则,通常对应一个签到天数节点,例如第 1 天、第 3 天、第 7 天等。

对于每一个签到天数节点,都可以单独配置对应的奖励内容,包括奖励类型、奖励数量等。这些规则之间是相互独立的,可以按需增加或调整,而不需要修改任何代码逻辑。
通过这种方式,一个签到玩法模板就可以完整描述一套签到规则。当后续创建活动时,如果活动需要使用签到玩法,只需要选择一个已经配置好的签到玩法模板,并将其挂载到活动中即可,活动本身不再关心具体的签到规则和奖励细节。
签到玩法在活动中的使用方式
我们在实际使用中,签到玩法模板通常是提前配置好的基础能力。创建活动时,只负责选择需要的签到模板,并控制活动的生效时间和投放范围。
这样做的好处是,玩法和活动实现了解耦 :
签到规则的调整不会影响活动逻辑,而活动的增加也不会导致签到逻辑重复实现。
抽奖玩法
抽奖玩法在活动中属于比较特殊的一类玩法,它和签到、任务最大的不同点在于:抽奖通常是由用户主动触发的,而不是被某个行为事件自动推进的。
在实际的活动设计中,抽奖玩法往往不会单独作为一个"任务"存在,而是和其他玩法进行组合使用。比如完成某些任务后获得抽奖道具,或者在活动期间每天登录可以获得一次抽奖机会,最终由用户在活动页面主动点击进行抽奖。
因此,在抽奖玩法的设计上,系统并不关心抽奖机会是如何获得的,而是只负责定义抽奖本身的规则。
抽奖玩法模板与规则配置
在后台中,抽奖玩法同样以「玩法模板」的形式进行管理。抽奖玩法列表页中展示的是当前已经配置好的抽奖模板,包括是否开启保底、保底次数、奖励数量等核心信息。

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

在奖励配置部分,可以为同一个抽奖模板配置多种奖励项,每个奖励项都可以设置对应的权重和数量,并支持标记为稀有奖励,用于配合保底规则使用。通过这种方式,抽奖玩法可以灵活地支持概率抽取、稀有奖励保底等常见需求。
需要说明的是,实际业务中的抽奖规则往往会更加复杂,比如多层奖池、分阶段保底、动态概率等。这里的示例主要是为了说明抽奖玩法的整体设计思路,因此只做了一个相对简化的 Demo 展示。
抽奖玩法在活动中的使用方式
在活动配置阶段,如果某个活动需要使用抽奖玩法,只需要选择一个已经配置好的抽奖玩法模板,并将其挂载到活动中即可。至于抽奖机会的来源,比如通过任务获得抽奖道具,还是通过其他方式发放,都不属于抽奖玩法本身的职责范围,而是由其他玩法或业务逻辑来完成。
通过这种拆分方式,抽奖玩法只关注"怎么抽"和"抽到什么",而不关心"为什么能抽",从而保持了玩法本身的独立性和可复用性。
任务玩法
随着活动逐渐变多,仅靠签到和抽奖这两类玩法,已经很难支撑后续的运营需求。
在实际运营过程中,常见的活动规则开始变得更加多样化,比如:
- 登录类行为(每日登录、累计登录)
- 消费/充值类行为
- 邀请好友、分享拉新
- 完成指定任务或行为次数
如果针对每一种行为都单独设计一套玩法,那么后台的玩法类型会迅速膨胀,不仅配置成本高,后续维护也会变得非常混乱。这显然不适合作为一个长期演进的活动中台方案。
基于这个背景,于是我们引入了任务玩法这一层抽象。
为什么要有任务玩法
任务玩法的核心目标,其实是承载多种"行为驱动型"的活动规则 。
它并不关心活动本身是什么样子,而是关注三个问题:
- 用户做了什么行为
- 这个行为是否满足某个规则
- 是否可以推进进度并发放奖励
相比签到玩法偏向时间维度、抽奖玩法偏向用户主动触发,任务玩法更像是一个事件驱动的规则容器。
任务玩法模板列表
在后台设计上,任务玩法同样以"模板"的形式存在,如下图所示:

- 每一行代表一个任务玩法模板
- 模板本身有明确的任务标识和任务类型
- 同一个活动中,可以挂载多个不同的任务玩法模板
这里我们可以看到登录任务、消费任务、邀请任务等都以统一的"任务玩法"形式存在,而不是拆成多个完全不同的玩法模块。
任务玩法的一个关键约束:一个模板只对应一种行为
在设计任务玩法时,有一个非常重要的约定:
每一个任务玩法模板,只绑定一种具体行为
例如:
- 登录任务模板,只处理登录行为
- 邀请任务模板,只处理邀请行为
- 充值任务模板,只处理充值行为
之所以做这样的限制,是因为不同行为下的规则模型和奖励结构差异非常大。 如果允许一个模板同时处理多种行为,那么在规则配置、进度计算和奖励发放上都会变得异常复杂,反而违背了中台"可配置、可扩展"的初衷。
这种设计方式的好处是:
- 模板本身职责清晰
- 规则配置逻辑统一
- 后续新增行为时,只需要新增对应的模板即可
活动层只需要按需挂载多个任务模板,就能组合出非常灵活的活动玩法。
任务玩法的规则与阶段奖励配置
在编辑某一个任务玩法模板时如下图,配置方式整体和签到玩法保持一致:


- 每个任务可以配置多个阶段
- 每个阶段有明确的达成条件(次数、天数、金额等)
- 每个阶段对应一组奖励配置
例如一个登录任务,可以配置:
- 第 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);
}
奖励发放并不是简单地"判断条件就发",而是基于规则节点进行,并通过唯一约束或幂等控制,确保同一个奖励只会被发放一次。
即使事件被重复处理,最终也不会出现重复发奖的问题。
九、事件到奖励的完整处理路径
回顾整个事件驱动的处理流程,我们就可以发现活动系统在运行阶段,始终围绕一条非常清晰的主线在工作:
用户行为先被抽象为事件,再由玩法消费事件,最后在活动维度下结算规则并发放奖励。
从系统职责划分上看:
- 业务系统只负责上报"发生了什么行为"
- 事件体系负责对行为进行标准化和分类
- 玩法模块负责解释行为并推进规则进度
- 活动系统负责限定生效范围和生命周期
- 奖励模块负责最终结果的幂等落库
也正是通过这种分层设计,用户行为、玩法规则和活动配置之间才能保持解耦。
新增行为时,不需要侵入活动逻辑;新增玩法时,也不需要改动业务代码。
为了更直观地理解这条链路在系统中的实际执行过程,我们下面用一张流程图,把一次事件从发生到最终发放奖励的整体路径串联起来。 
我们可以看到,签到和任务这类被动行为统一走事件驱动链路,而抽奖作为一种主动玩法,虽然执行路径不同,但最终仍会以事件的形式回流到活动系统中,用于统计、联动或后续规则判定,形成一个完整闭环。
正是通过这条事件驱动链路,活动中台才能在不侵入业务代码的前提下,支撑复杂玩法组合和高频运营调整。
关于活动中台落地的一些补充说明
到这里为止,这套活动系统在设计层面其实已经是完整的了。无论是事件驱动、玩法拆分,还是活动和奖励的组合方式,从结构上看都已经可以覆盖大多数常见场景。
但放到真实业务里,会发现活动系统往往比设计阶段要复杂得多,也更"厚重"一些。运营节奏快、需求变化频繁,很多规则并不是一开始就能想清楚,而是在不断上线、调整和复盘的过程中逐步演化出来的。
比如在实际运行中,事件重复上报几乎是必然会发生的事情。网络重试、客户端异常、异步消费失败,都会导致同一个行为被多次处理。如果在设计阶段没有把幂等控制放到用户数据这一层,后面很容易出现进度错乱或者重复发奖的问题。
另外,活动一旦上线,就很难再"完全推倒重来"。玩法配置和页面资源可以调整,但已经产生的用户进度和奖励,本质上是一种已经发生的事实,只能在此基础上向前兼容,而不能简单覆盖或重置。这也是为什么在活动系统里,会刻意区分"配置数据"和"运行数据",避免两者相互影响。
抽奖玩法在实际业务中也往往比预期复杂。除了概率和保底规则,还会涉及抽奖道具来源、每日次数限制、风控校验等问题。这类逻辑更接近交易行为,而不是单纯的状态推进,因此在设计时会选择让抽奖走一条相对独立的执行路径,而不是强行并入通用事件链路。
还有一个很现实的情况是,运营配置几乎一定会发生临时调整。比如活动延长、奖励内容修改、投放范围变更等,这就要求活动的发布和投放策略具备版本化和可回溯能力,而不是简单地"改一条配置就生效"。
也正是基于这些实际情况,这套活动系统在设计时才会刻意把行为、玩法和活动拆得比较松。很多复杂度并不是一次性解决的,而是通过清晰的边界,把变化控制在局部范围内,让系统在高频活动和持续迭代的情况下,仍然有足够的弹性继续演进。
结语
我们在把项目的活动系统进行拆分重构后,就如上文设计的那样,文中展示的玩法拆分、事件驱动和活动组合方式,本质上是一种思路层面的整理。它解决的并不是"怎么把所有活动一次性设计完",而是试图回答一个更现实的问题:当活动不断叠加、规则不断变化时,系统还能不能保持可控。
需要说明的是,我这里的设计并不是放之四海而皆准的标准答案。不同项目的业务形态、用户规模、运营节奏都不一样,实际落地时一定需要结合自身情况进行取舍和调整。有些项目可能不需要这么细的玩法拆分,有些项目可能会在事件层引入更多复杂规则,这些都很正常。
所以这套设计更像是一次重构过程中的阶段性结果,而不是终点。随着业务发展,玩法会继续变多,规则会继续变复杂,系统本身也需要不断演进。至少在当前阶段,这样的结构能让活动系统在高频运营和持续迭代的情况下,保持相对清晰的边界,而不至于失控。