现在不管是游戏、app,任务签到系统都是一个绕不开的需求,是一种运营留存手段。工作中很多朋友都会问我如何去设计一个通用的任务系统,我都会一一做解答。今天写这篇文章,也是把我的经验分享给大家,给大家提供一个参考思路
一般我们的任务系统都是事件驱动的,比如用户进入app,胜利了几场游戏,每日签到,结交了几个朋友,充值了几次钱等等
在系统设计中,以上的这些事件我们有的通过app上报,有的是系统内的业务产生的一些附加消息,这些消息会通过mq在系统内解耦,消息系统对所关心的消息进行监听。大体的设计思路是这样的:
任务中心架构设计
任务对象设计
任务一般是运营进行配置,我们需要对任务对象进行设计,一般的任务由以下几个重要的字段组成:触发事件(类型)、触发阶段(次数)、触发类型(连续、累计)、阶段奖励等;程序需要对触发事件和触发类型进行逻辑实现,管理平台通过选择的方式基于触发事件(类型)、触发阶段(次数)、触发类型(连续、累计)、阶段奖励来组合不同的任务,大体的数据结构如下:
type (
// 任务对象
TaskObject struct {
Id int64 `json:"id"` // 任务id
Name string `json:"name"` // 任务名称
Icon string `json:"icon"` // 任务小图标
AttrCat string `json:"attr_cat"` // 任务属性类别 参考enum.TaskAttributeCat
Category string `json:"category"` // 任务分类 参考enum.TaskType
TriggerType string `json:"trigger_type"` // 触发类型 参考enum.TaskTriggerType
FinishType string `json:"finish_type"` // 完成类型 参考enum.TaskFinishType
Status int64 `json:"status"` // 状态 0:正常 1:无效
Stage int64 `json:"stage"` // 阶段
Rewards []*RewardConfig `json:"rewards"` // 奖励配置
}
RewardConfig struct {
Stage int64 `json:"stage"` // 阶段
Rewards []*Reward `json:"rewards"` // 奖励
}
Reward struct {
GoodsType int64 `json:"goods_type"` // 物品类型 1:道具 2:金币 3:钻石 4:主持币 5:收入 6:活跃值 7:积分
GoodsId int64 `json:"goods_id"` // 物品id,物品类型为道具的时候有效
GoodsName string `json:"goods_name"` // 物品名称
GoodsPicUrl string `json:"goods_pic_url"` // 物品图片
GoodsDesc string `json:"goods_desc"` // 物品描述
GoodsNum int64 `json:"goods_num"` // 数量
Expired int64 `json:"expired"` // 过期时间,物品类型为道具的时候有效(单位是天)
}
)
设计完任务对象的数据结构以后,我们需要设计用户任务数据,用户针对某个任务是否完成,完成到那个阶段,完成以后的奖励发放,完成阶段重置等,都是基于用户任务参与数据来的,接下来我们来做用户任务数据存储设计。
任务数据存储设计
整个任务参与数据,我们全部用redis来实现,这里有几个好处
- 处理快
- 可以很好地利用redis的有效期缓存,自动清理数据
我们以每日/周任务为例,来看这类任务的缓存设计是怎么做的,每日任务是指用户当日是否触发某个事件几次来标示用户是否完成任务,完成任务以后用户是否领取奖励(也有自动发放奖励的需求);昨天的数据不在今日/周起效(按天/周来统计);那么针对这样的需求,我们的缓存大概是这样的设计:
每日/周缓存设计
这个设计里面,可能有很多用户会觉得用K/V来做?是的,没有利用redis很复杂的数据对象;(redis最高效的其实就是sting的存取)这里的key和value组成意义可以参考上图设计,程序里面会做分割处理,具体代码如下:
func (t *taskDaily) dealWith(userId int64, triggerType enum.TaskTriggerType, num int64) {
nowDayStr := time.Now().Format("20060102")
// 获取过期时间(今日的最后一秒+1秒)
expired := tools.GetCurrentDayLeftoverSecond() + 1
// 获取该任务类型对应的所有用户任务
tasks := t.FindTaskWithTrigTp(enum.TaskACUser, triggerType)
t.dealWithUserTask(userId, num, nowDayStr, expired, tasks)
if t.TSharerInf.IsHost(userId) {
// 获取该任务类型对应的所有主持人任务
tasks = t.FindTaskWithTrigTp(enum.TaskACHost, triggerType)
t.dealWithHostTask(userId, num, nowDayStr, expired, tasks)
}
}
// 个人任务处理
func (t *taskDaily) dealWithUserTask(userId int64, num int64, nowDayStr string, expired int, tasks []*TaskObject) {
for _, task := range tasks {
// 获取用于对于该任务的缓存key
cacheKey := fmt.Sprintf(t.cacheKeyPrefix, task.Id, userId, nowDayStr)
// 获取任务的缓存数据(完成状态、领取状态、阶段数)
fs, rs, cnt, err := t.getStatus(cacheKey)
if err != nil {
continue
}
// 判断任务是否完成或者已领取
if fs == enum.FsFinished || rs == enum.RsReceived {
// 已完成,直接略过
continue
}
// 计算逻辑阈值
cnt = t.StatisticsCnt(cnt, num, task)
// 已完成值与任务配置的值进行比较
var value string
if task.Stage > cnt {
// 未完成,继续累计次数
value = fmt.Sprintf("%s:%s:%d", enum.FsUnFinish, enum.RsUnReceive, cnt)
} else {
// 已完成,重置完成标识
value = fmt.Sprintf("%s:%s:%d", enum.FsFinished, enum.RsUnReceive, cnt)
// 推送任务完成事件
t.TSharerInf.PushEvent(userId, cnt, enum.TPFinished, task)
}
// 设置缓存
_ = t.SetCacheValue(cacheKey, value, expired)
}
}
以上是整个事件触发的核心处理代码,是不是很简单?
当然,除了每日、每周任务,还有其他任务,比如签到任务、每月任务、成长任务等,大体思路都是一样的,我给出具体的缓存实现:
签到类的缓存设计
成长任务缓存设计
总结
基于以上思路,满足了整个公司所有app的任务需求,整个任务中心代码量不到两千行代码,上线以后稳定运行,没有出现一次问题。
大家在开发的过程中任务系统是怎么实现的呢?欢迎大家转发评论讨论
最新评论