签到功能实现,没有你想的那么复杂(一)
1.签到定义以及作用
签到
,指在规定的簿册上签名或写一“到”字,表示本人已经到达。在APP中使用此功能,可以增加用户粘性和活跃度.
2.技术选型
redis为主写入查询,mysql辅助查询. 传统签到多数都是直接采用mysql为存储DB,在大数据的情况下数据库的压力较大.查询速率也会随着数据量增大而增加.所以在需求定稿以后查阅了很多签到实现方式,发现用redis做签到会有很大的优势.本功能主要用到redis位图,后面我会详细讲解实现过程.
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(Integer userId) { int signCount = 0; LocalDate date = LocalDate.now(); String signKey = String.format(RedisKeyConstant.USER_SIGN_IN, date.getYear(), userId); //这里取出的是位图一个偏移值区间的值,区间起始值为当月的第一天,范围值为当月的总天数(参考命令bitfield) ListLong list = cacheClient.getBit(signKey, date.getMonthValue() * 100 + 1, date.getDayOfMonth()); if (list != null list.size() 0) { //可能该用户这个月就没有签到过,需要判断一下,如果是空就给一个默认值0 long v = list.get(0) == null ? 0 : list.get(0); for (int i = 0; i date.getDayOfMonth(); i++) { //如果是连续签到得到的long值右移一位再左移一位后与原始值不相等,连续天数加一 if (v 1 1 == v) return signCount; signCount += 1; v = 1; } } return signCount; }
记录签到积分类型以及连续签到积分
public Boolean doSaveUserIntegral(int userId, int signContinuousCount) { int count = 0; //叠加签到次数 cacheClient.incrValue(String.format(RedisKeyConstant.USER_SIGN_IN_COUNT, userId)); ListUserIntegralLog userIntegralLogList = new LinkedList(); userIntegralLogList.add(UserIntegralLog.builder() .createTime(LocalDateTime.now()) .operationTime(LocalDate.now()) .bak(BusinessConstant.Integral.NORMAL_SIGN_COPY) .integral(BusinessConstant.Integral.SIGN_TYPE_NORMAL_INTEGRAL) .integralType(BusinessConstant.Integral.SIGN_TYPE_NORMAL) .userId(userId) .build()); count += BusinessConstant.Integral.SIGN_TYPE_NORMAL_INTEGRAL; //连续签到处理,获取缓存配置连续签到奖励 //因为每个月的天数都不是固定的,连续签到奖励是用的redis hash写入的.所以这个地方用32代替一个月的连续签到天数,具体配置在下方图中 if (signContinuousCount == LocalDate.now().lengthOfMonth()) { signContinuousCount = 32; } MapString, String configurationHashMap = cacheClient.hgetAll("userSign:configuration"); String configuration = configurationHashMap.get(signContinuousCount); if (null != configuration) { int giveIntegral = 0; JSONObject item = JSONObject.parseObject(configuration); giveIntegral = item.getInteger("integral"); if (giveIntegral != 0) { if (signContinuousCount == 32) { signContinuousCount = LocalDate.now().lengthOfMonth(); } userIntegralLogList.add(UserIntegralLog.builder() .createTime(LocalDateTime.now()) .bak(String.format(BusinessConstant.Integral.CONTINUOUS_SIGN_COPY, signContinuousCount)) .integral(giveIntegral) .integralType(BusinessConstant.Integral.SIGN_TYPE_CONTINUOUS) .userId(userId) .build()); count += giveIntegral; } } //改变总积分和批量写入积分记录 return updateUserIntegralCount(userId, count) userIntegralLogService.saveBatch(userIntegralLogList); }
连续签到获取的积分配置以及文案配置
4.1.4 实现补签
补签功能是一个签到补充功能,主要就是方便用户在忘了签到的情况下也能通过补签功能达到相应的连续签到条件,从而得到奖励. 补签主方法
//day表示需要补签的日期,因为我们平台的签到周期是一个月所以只需要传日的信息就可以,入 7号传入7 public ResponseResult saveSignInRetroactive(Integer userId, Integer day) { Boolean result = Boolean.TRUE; ResponseResult responseResult = ResponseResult.newSingleData(); responseResult.setMessage("今日无需补签哟"); responseResult.setCode((byte) -1); LocalDate timeNow = LocalDate.now(); //检测是否补签达上限 String retroactiveKey = String.format(RedisKeyConstant.USER_RETROACTIVE_SIGN_IN, timeNow.getMonthValue(), userId); //从redis中取出用户的当月补签的集合set.我们平台的限制是三次补签 SetString keys = cacheClient.hkeys(retroactiveKey); if (CollUtil.isNotEmpty(keys) keys.size() == 3) { responseResult.setMessage("本月补签次数已达上限"); result = Boolean.FALSE; } //检查补签积分是否足够,这里就是一个简单的单表查询,用于查询积分是否足够本次消耗 UserIntegral userIntegral = userIntegralService.getOne(new LambdaQueryWrapperUserIntegral().eq(UserIntegral::getUserId, userId)); //这里只是简单的做了一个map放置三次补签分别消耗的积分(key:次数 value:消耗积分),也可参照之前连续签到配置放入redis缓存中便于后台管理系统可配置 Integer reduceIntegral = getReduceIntegral().get(keys.size() + 1); if (reduceIntegral userIntegral.getIntegral()) { responseResult.setMessage("您的橙汁值不足"); result = Boolean.FALSE; } if (result) { LocalDate retroactiveDate = LocalDate.of(timeNow.getYear(), timeNow.getMonthValue(), day); String signKey = String.format(RedisKeyConstant.USER_SIGN_IN, timeNow.getYear(), userId); long monthAndDay = Long.parseLong(retroactiveDate.format(DateTimeFormatter.ofPattern("MMdd"))); //后端检测是否用户今日签到过同时补签日期不可大于今天的日期 if (!cacheClient.getBit(signKey, monthAndDay) timeNow.getDayOfMonth() day) { boolean oldResult = cacheClient.setbit(signKey, monthAndDay); if (!oldResult) { //补签记录(:月份) 过月清零,过期时间是计算出当前时间的差值,补签次数是一个月一刷新的 cacheClient.hset(retroactiveKey, retroactiveDate.getDayOfMonth() + "", "1", (Math.max(retroactiveDate.lengthOfMonth() - timeNow.getDayOfMonth(), 1)) * 60 * 60 * 24); //这里就是对积分总表减少.以及对积分记录进行记录.参照下方代码块 doRemoveUserIntegral(userId, reduceIntegral, RETROACTIVE_SIGN_COPY); responseResult.setCode((byte) 0); responseResult.setMessage("补签成功"); } } } return responseResult; }
积分减少并写入积分变动记录
public Boolean doRemoveUserIntegral(int userId, int reduceIntegral, String bak) { return updateUserIntegralCount(userId, -reduceIntegral) userIntegralLogService.save(UserIntegralLog.builder() .createTime(LocalDateTime.now()) .operationTime(LocalDate.now()) .bak(bak) .integral(-reduceIntegral) .integralType(BusinessConstant.Integral.RETROACTIVE_SIGN_COPY.equals(bak) ? BusinessConstant.Integral.SIGN_TYPE_RETROACTIVE : BusinessConstant.Integral.SIGN_TYPE_WELFARE) .userId(userId) .build()); }
至此一个签到补签
的完整流程就做好了.之后的文章将会介绍签到日历
和签到任务
的解决方案.欢迎持续关注

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
一次zuul版本升级产生的问题排查记录
起因 事情的起因是由于早期的一些服务版本放到现在太低了,基本上都是SpringBoot1.5.x,因此准备统一对服务进行一次版本升级,升级到2.1.x,SpringCloud``版本升级到Greenwich。当然我们用的旧版本的zuul相关的都需要升级。 意外的Bug 我们网关使用的是zuul,使用的是spring-cloud-netflix封装的包,此次版本升级同步升级了相关的包。但是意外的情况发生了,在测试环境上我们发现上传文件会出现异常。具体表现是这样的:当上传的文件超出一定大小后,在经过zuul网关并向其他服务转发的时候,之前上传的包就不见了。这个情况十分奇怪,因此马上开始排查。 Bug的排查 出现这样的问题,第一反应是测试是不是根本没有上传包所以当然包没法转发到下一层,当然这种想法很快被否定了。好吧,那就认真的排查吧。 首先先去追踪了一下路由以及出现的具体日志,将问题定位到zuul服务,排除了上游nginx和下游业务服务出现问题的可能。但是zuul服务没有任何异常日志出现,所以非常困扰。检查过后发现文件确实有通过zuul,但是之后凭空消失没有留下一点痕迹。 明明当初考虑上传文...
- 下一篇
如何利用go-zero在Go中快速实现JWT认证
关于JWT是什么,大家可以看看官网,一句话介绍下:是可以实现服务器无状态的鉴权认证方案,也是目前最流行的跨域认证解决方案。 要实现JWT认证,我们需要分成如下两个步骤 客户端获取JWT token。 服务器对客户端带来的JWT token认证。 1. 客户端获取JWT Token 我们定义一个协议供客户端调用获取JWT token,我们新建一个目录jwt然后在目录中执行 goctl api -o jwt.api,将生成的jwt.api改成如下: type JwtTokenRequest struct { } type JwtTokenResponse struct { AccessToken string `json:"access_token"` AccessExpire int64 `json:"access_expire"` RefreshAfter int64 `json:"refresh_after"` // 建议客户端刷新token的绝对时间 } type GetUserRequest struct { UserId string `json:"userId"` } typ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS8编译安装MySQL8.0.19
- CentOS关闭SELinux安全模块
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS6,CentOS7官方镜像安装Oracle11G