1. 签到定义以及作用
签到,指在规定的簿册上签名或写一“到”字,表示本人已经到达。在APP中使用此功能,可以增加用户粘性和活跃度。
一个有签到功能的APP,往往会提供补签功能,连续签到多少天会给予相关的奖励;而为了进一步增加用户粘性,还会提供签到任务功能,完成任务也可获取对应的奖励。
功能用例
本文带你实现一个包含上述用例的签到功能,看完以后你会发现,签到,没有你想的那么复杂!
2. 技术选型
redis为主写入查询,mysql辅助查询。传统签到多数都是直接采用mysql为存储DB,在大数据的情况下数据库的压力较大。查询速率也会随着数据量增大而增加。所以在需求定稿以后查阅了很多签到实现方式,发现用redis做签到会有很大的优势。
本功能主要用到redis位图[1],后面我会详细讲解实现过程。
3. 实现效果
这里抛砖引玉,展示一下我们app的签到实现效果
4 功能实现
功能大致分为两个大模块
-
签到流程(签到,补签,连续,签到记录)
-
签到任务(每日任务,固定任务)
签到流程图如下:
4.1.1 表设计
因为大部分功能使用redis存储,使用到mysql主要是为了存储用户总积分以及积分记录,便于查询签到记录和用户总积分
CREATE TABLE `t_user_integral` (
`id` varchar(50) NOT NULL COMMENT 'id',
`user_id` int(11) NOT NULL COMMENT '用户id',
`integral` int(16) DEFAULT '0' COMMENT '当前积分',
`integral_total` int(16) DEFAULT '0' COMMENT '累计积分',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户积分总表'
CREATE TABLE `t_user_integral_log` (
`id` varchar(50) NOT NULL COMMENT 'id',
`user_id` int(11) NOT NULL COMMENT '用户id',
`integral_type` int(3) DEFAULT NULL COMMENT '积分类型 1.签到 2.连续签到 3.福利任务 4.每日任务 5.补签',
`integral` int(16) DEFAULT '0' COMMENT '积分',
`bak` varchar(100) DEFAULT NULL COMMENT '积分补充文案',
`operation_time` date DEFAULT NULL COMMENT '操作时间(签到和补签的具体日期)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户积分流水表'
4.1.2 redis key设计
//人员签到位图key,一个位图存一个用户一年的签到状态,以userSign为标识,后面的两个参数是今年的年份和用户的id
public final static String USER_SIGN_IN = "userSign:%d:%d";
//人员补签key,一个Hash列表存用户一个月的补签状态,以userSign:retroactive为标识,后面的两个参数是当月的月份和用户的id
public final static String USER_RETROACTIVE_SIGN_IN = "userSign:retroactive:%d:%d";
//人员签到总天数key,以userSign:count为标识,后面的参数是用户的id
public final static String USER_SIGN_IN_COUNT = "userSign:count:%d";
4.1.3 实现签到
接口restful的形式,头信息里传入用户id
@ApiOperation("用户签到")
@PostMapping("/signIn")
@LoginValidate
public ResponseResult saveSignIn(@RequestHeader Integer userId) {
return userIntegralLogService.saveSignIn(userId);
}
sevice实现层
public ResponseResult saveSignIn(Integer userId) {
//这里是我们的公司统一返回类
ResponseResult responseResult = ResponseResult.newSingleData();
//用String.format拼装好单个用户的位图key
String signKey = String.format(RedisKeyConstant.USER_SIGN_IN, LocalDate.now().getYear(), userId);
//位图的偏移点为当天的日期,如今天,偏移值就是1010
long monthAndDay = Long.parseLong(LocalDate.now().format(DateTimeFormatter.ofPattern("MMdd")));
responseResult.setMessage("今日已签到");
responseResult.setCode((byte) -1);
//检测是否用户今日签到过,用getBit可以取出该用户具体日期的签到状态(位图的值只有两个,1或者0,这里1代表true)
if (!cacheClient.getBit(signKey, monthAndDay)) {
//位图的set方法会返回该位图未改变前的数值,这里如果之前没有签到过默认是0,也就是false
boolean oldResult = cacheClient.setbit(signKey, monthAndDay);
if (!oldResult) {
//计算出这个月该用户的到今天的连续签到天数,此方法参照下方计算连续签到天数的代码块
int signContinuousCount = getContinuousSignCount(userId);
//此方法参照下方记录签到积分类型以及连续签到积分代码块
doSaveUserIntegral(userId, signContinuousCount);
responseResult.setCode((byte) 0);
}
}
return responseResult;
}
计算连续签到天数
/**
* @description: 获取连续签到天数
* @author: chenyunxuan
* @updateTime: 2020/8/25 4:43 下午
*/
private int getContinuousSignCount(I