大话业务场景与解决方案:做任务
背景
多数的移动端 APP 都会有做任务领取奖励的功能模块,这类需求的目的是培养用户使用习惯,提升用户活跃性,用户完成任务获得积分奖励,通过积分兑换商品或者充值话费,微信体现等。
拟定需求场景(如图 ↓),概要:APP 底部导航中新增小任务 Tab,点击 Tab 可查看任务完成进度和领取情况,点击去完成跳转到做任务的业务界面,当用户完成任务并且满足领取条件的时候,任务 Tab 需要红点提醒用户当前有奖励可领取,用户领取后并且当前没有待领取奖励小红点消失,任务完成进度和领取状态仅保持当天,隔天刷新。
业务分析
在开发前需要对需求进行整理,对细节进行确认,然后设计解决方案,预估开发时间,这里将对于业务中核心的内容进行梳理:
-
用户想要完成任务,需要去操作其他业务功能,如:评论成功后需要完成每日评论任务,关注主题后完成关注新手任务,这里就涉及核心问题,任务需要依赖于其他业务
-
为了保障后续拓展性,任务需要支持后台管理,配置任务名,描述,任务类型(每日,新手,活动),完成次数,奖励积分数量,去完成跳转 uri 等
-
用户完成任务后不用自动领取奖励,需要进入到任务列表点击领取操作,可领取时导航 Tab 需要小红点提醒,和产品确认任务的完成和提醒的用户体验 可以接受短时间延迟
-
用户多次操作业务,或者出现重复操作(恶意并发请求刷积分),保证任务只能完成一次并且只能领取一次奖励,需要保证幂等性
方案设计
核心目标:
- 任务依赖其他业务,需要进行解耦,不影响其他业务的功能和性能
- 设计后台可管理,便于后续拓展
- 抽象任务模块,代码抽象开发
- 完成任务和领取需要保证幂等性
- 高可用
名词定义:
事件
- 任务中涉及依赖其他业务,这里需要抽象出一个概念,用户通过操作业务,完成任务的这个操作,我们把这个过程定义为用户完成任务事件触发完成,如:评论事件,点赞事件,关注事件,等
解决方案:
在实现方案上,采用异步消耗队列的方式,依赖业务接口埋入事件上报,将用户成功操作业务的任务事件上报到队里中,然后开发消息消耗的脚本程序,对消息中用户触发的任务事件进行业务逻辑处理和 DB 操作,更新用户任务进度和可领取状态,响应给用户(完成任务红点提醒),设计图:
- 依赖业务解耦
- 依赖业务将操作成功用户的任务事件上报到消息队列,然后程序进行异步消耗
- 方案解决了依赖业务之间的强耦合,并且基本不影响现有依赖业务的接口性能
- 高可用:
- 通过调度系统启动多进程对队列进行消耗
- 进程守护系统,守护进程保活,奔溃重启,可对执行日志进行记录与查看
- 如: gocron
- 消息队列监控,无法及时消耗进行预警,保障即时性,避免长时间的延迟
- rabbitmq
- redis list
- … …
- 容错与补偿
- 队列消耗失败需要进行记录,并可根据业务场景,通过另外程序进行补充处理
- 用户操作业务上报任务事件不限制次数,以免用户没完成任务,允许用户重新尝试去做任务,程序消耗需要控制任务只能完成一次
- 通过调度系统启动多进程对队列进行消耗
表结构:
-- 任务表
CREATE TABLE `task` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增',
`icon` varchar(300) NOT NULL DEFAULT '' COMMENT '图标',
`title` varchar(30) NOT NULL DEFAULT '' COMMENT '任务标题',
`type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '任务类型,新手任务=1,每日任务=2',
`event` int(11) NOT NULL DEFAULT '0' COMMENT '事件',
`des` varchar(30) NOT NULL DEFAULT '' COMMENT '任务描述',
`target_num` int(11) NOT NULL DEFAULT '0' COMMENT '目标数量',
`points` int(11) NOT NULL DEFAULT '0' COMMENT '金币',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 0=下线,1=上线,-1=删除',
`app_version` varchar(15) NOT NULL DEFAULT '' COMMENT 'app版本号',
`app_version_compare` varchar(10) NOT NULL DEFAULT '' COMMENT 'app版本号比较运算符',
`operator` varchar(10) NOT NULL DEFAULT '' COMMENT '操作人',
`jump_uri` varchar(300) NOT NULL DEFAULT '' COMMENT '跳转协议',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`event_begin` timestamp NOT NULL DEFAULT '1970-01-02 00:00:00' COMMENT '事件开始时间',
`event_end` timestamp NOT NULL DEFAULT '1970-01-02 00:00:00' COMMENT '事件结束时间',
`task_begin` timestamp NOT NULL DEFAULT '1970-01-02 00:00:00' COMMENT '任务开始时间',
`task_end` timestamp NOT NULL DEFAULT '1970-01-02 00:00:00' COMMENT '任务结束时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户任务情况表
CREATE TABLE `user_task_case` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户任务情况',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
`task_id` int(11) NOT NULL DEFAULT '0' COMMENT '任务id',
`task_type` int(11) NOT NULL DEFAULT '0' COMMENT '任务类型',
`event` int(11) NOT NULL DEFAULT '0' COMMENT '事件',
`task_uni` varchar(30) NOT NULL DEFAULT '' COMMENT '任务唯一标识(唯一约束) ',
`target_num` int(11) NOT NULL DEFAULT '0' COMMENT '目标数量',
`finish_num` int(11) NOT NULL DEFAULT '0' COMMENT '完成数量',
`points` int(11) NOT NULL DEFAULT '0' COMMENT '可领取金币数量',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 0=待完成,1=待领取,2=已经领取',
`finish_at` timestamp NOT NULL DEFAULT '1970-01-02 00:00:00' COMMENT '完成任务时间',
`get_at` timestamp NOT NULL DEFAULT '1970-01-02 00:00:00' COMMENT '领取时间',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_user_id_task_uni` (`user_id`,`task_uni`) USING BTREE,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户任务情况表';
更新语句:
-- 更新领取状态,注意:WHERE条件,强校验
UPDATE user_task_case
SET `status`=2,finish_at=CURRENT_TIMESTAMP
WHERE id=:id AND user_id=:user_id AND `status`=1 AND finish_num>=target_num
表重点字段说明:
-
task_uni
- 任务唯一标识
-
user_id 和 task_uni 组合唯一约束索引
- 每日任务:
- 任务 id任务类型日期(task_id_type_date)
- 默认都是只做一次(新手任务/活动任务):
- 任务 id(task_id)
- 每日任务:
-
幂等性
- user_task_case 表中 user_id 和 task_uni 组合唯一约束索引,通过 mysql 的唯一约束,保证了多进程并行消耗事件队列的情况下,每日任务和一次性任务不能重复 INSERT
- 通过 UPDATE 的 WHERE 条件校验保障领取的幂等性
管理后台:
产品在规划需求的时候会设计出相关后台,但是不一定设计的合理,所以这里需要根据确认的解决方案协助产品对于管理后台进行调整,保障后续的拓展性
代码层面:
- 面向抽象开发,合理使用设计模式,便于后续的拓展
话外篇:
谈近一年的感悟,近一年参与了新 APP 项目的开发,从 0 开始搭建项目,看着 DAU 一点点儿的涨起来,还是挺有成就感的。
角色上产生了变化,现在感觉自己更像是一个项目的参与者,而不是任务的执行人,完成业务开发的同时也会对产品上有根深了解。
空闲时间也会对竞品调研以及用户使用意见或者问题进行跟进,站在用户角度提供产品上的一些建议。
后续会把新项目开发过程中遇到问题或者常见的业务场景下的解决方案进行梳理出来进行博文分享。