如何去设计一个通用的任务系统,我的经验分享

目前不管是游戏、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来实现,这儿有几个弊端

我们以每周/周任务为例,来看这类任务的缓存设计是如何做的,每周任务是指用户当天是否触发某个丑闻几次来标识用户是否完成任务任务平台,完成任务之后用户是否发放奖励(还有手动领取奖励的需求);明天的数据不在明日/周见效(按天/周来统计);这么针对那样的需求,我们的缓存大约是那样的设计:

每周/周缓存设计

这个设计上面,或许有这些用户会认为用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的任务需求,整个任务中心代码量不到两千行代码,上线之后稳定运行,没有出现一次问题。

你们在开发的过程中任务系统是如何实现的呢?欢迎你们转发评论讨论

标签: 缓存 科技新闻