签到业务流程

news2024/11/24 9:07:41

1.技术选型

Redis主写入查询,Mysql辅助查询,传统签到多数都是直接采用mysql为存储DB,在大数据的情况下数据库的压力较大.查询速率也会随着数据量增大而增加.所以在需求定稿以后查阅了很多签到实现方式,发现用redis做签到会有很大的优势.本功能主要用到redis位图

2.功能实现

1 签到流程(签到,补签,连续,签到记录)
2 签到任务(每日任务,固定任务)
在这里插入图片描述

3.表设计

## 用户积分总表
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.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";

5.实现签到

5.1controller

@ApiOperation("用户签到")
    @PostMapping("/signIn")
    @LoginValidate
    public ResponseResult saveSignIn(@RequestHeader Integer userId) {
        return userIntegralLogService.saveSignIn(userId);
    }

5.2service

1创建一个 ResponseResult 对象,用于统一返回结果。

2使用 String.format 方法拼接出该用户的签到位图在 Redis 中的 Key,格式为 USER_SIGN_IN:年份:用户ID。

3获取当前日期的月份和日期,转换为格式为 MMdd 的长整型数字,作为位图的偏移点。

4设置默认的响应结果信息为 “今日已签到”,并将 code 设置为 -1。

5使用 cacheClient.getBit 方法检查用户今日是否已经签到过。如果返回值为 false,表示用户今日尚未签到。

6执行 cacheClient.setbit 方法设置位图中对应位置为 1,表示用户今日已签到。该方法会返回设置前的位图值。

7如果之前没有签到过(即 oldResult 为 false),则计算该用户本月至今天的连续签到天数,调用 getContinuousSignCount 方法。

8执行 doSaveUserIntegral 方法,记录用户的签到积分类型以及连续签到积分。具体实现不在代码段中。

将响应结果的 code 设置为 0,表示签到成功。

9返回最终的响应结果。

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;
    }

5.3连续签到
流程: 1.本质就是取出位图一个偏移值区间内的值,区间起始值为当月的第一天,范围为当月的总天数(BITFIELD命令)——>2.如果没有签到呢默认0,若是连续签到则将得到的long值右移一位再左移一位,若是不相等signCount+1;
1首先,根据传入的用户 ID 和当前时间,生成用户在 Redis 中保存签到信息的 Key,然后从 Redis 中获取该用户在本月内的签到信息的位图。

2接着,遍历该位图,从第一天到当前日期,判断该用户在每一天是否进行了签到。位图中每一个值都表示一天是否签到,如果为 1 表示签到,为 0 则表示没有签到。

3在遍历位图的过程中,如果某一天没有签到,则返回当前的连续签到天数。如果该用户从本月的第一天开始一直到当前日期都进行了签到,则返回当前日期所在的连续签到天数。

如果该用户在本月尚未进行过签到,则返回连续签到天数为 0。
如何判断连续签到?

1通过遍历位图的方式,从第一天到当前日期(date.getDayOfMonth()),依次检查每一天的签到情况。

2对于每一天的检查,首先从位图中获取对应的值,即 list.get(i)。如果该值为 null,则默认设置为 0,表示该天没有进行签到。

3判断获取到的值 v 是否满足连续签到的条件:右移一位再左移一位后与原始值不相等。如果相等,说明当前位为 0,表示当天没有签到,返回当前的连续签到天数。

4如果满足连续签到的条件,将连续签到天数 signCount 加一。

5将值 v 右移一位,用于下一天的判断。

遍历完所有的天数后,返回连续签到天数 signCount。

 private int getContinuousSignCount(Integer userId) {
        int signCount = 0;
        LocalDate date = LocalDate.now();
        String signKey = String.format(RedisKeyConstant.USER_SIGN_IN, date.getYear(), userId);
        //这里取出的是位图一个偏移值区间的值,区间起始值为当月的第一天,范围值为当月的总天数(参考命令bitfield)
        List<Long> 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值右移一位再左移一位后与原始值不相等,连续天数加一(相等说明当前位为0没有签到过了)
                if (v >> 1 << 1 == v) return signCount;
                signCount += 1;
                v >>= 1;
            }
        }
        return signCount;
    }

5.4记录积分类型和连续签到的积分

public Boolean doSaveUserIntegral(int userId, int signContinuousCount) {
        int count = 0;
        //叠加签到次数
        cacheClient.incrValue(String.format(RedisKeyConstant.USER_SIGN_IN_COUNT, userId));
        List<UserIntegralLog> 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;
        }
        Map<String, 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);
    }

6.补签

补签功能是一个签到补充功能,主要就是方便用户在忘了签到的情况下也能通过补签功能达到相应的连续签到条件,从而得到奖励.
这段代码是一个补签功能的后端实现,主要用于用户在签到平台上进行补签操作。我来详细解释一下它的业务流程:

1首先,根据传入的用户ID和需要补签的日期信息,判断今日是否需要进行补签。如果不需要补签,则返回相应的提示信息并结束流程。

2接着,从 Redis 中获取用户当月已经补签的次数,如果已经达到三次补签上限,则返回相应的提示信息,并将结果设置为失败。

3然后,检查用户的积分情况,确保用户的积分足够进行本次补签所需的积分消耗。如果积分不足,则返回相应的提示信息,并将结果设置为失败。

4如果前面的步骤都通过了,那么表示用户可以进行补签操作。在这里会先构建补签日期的 LocalDate 对象,并生成相应的 Redis Key 来存储用户的签到信息。

5在进行补签前,会对用户今日是否已经签到过进行检测,同时也会检测补签的日期是否大于今天的日期。如果通过检测,则将用户的签到信息在 Redis 中进行设置。

6如果补签成功,将补签的记录存入 Redis,同时更新用户的积分信息,并返回补签成功的提示信息。

整体来说,这段代码主要是通过 Redis 存储用户的签到信息及补签记录,并结合用户的积分情况来进行补签操作的逻辑。

//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.我们平台的限制是三次补签
        Set<String> keys = cacheClient.hkeys(retroactiveKey);
        if (CollUtil.isNotEmpty(keys) && keys.size() == 3) {
            responseResult.setMessage("本月补签次数已达上限");
            result = Boolean.FALSE;
        }
        //检查补签积分是否足够,这里就是一个简单的单表查询,用于查询积分是否足够本次消耗
        UserIntegral userIntegral = userIntegralService.getOne(new LambdaQueryWrapper<UserIntegral>().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() - retroactiveDate.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());
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1415693.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM系列——基础知识

Java运行区域 程序计数器&#xff08;Program Counter Register&#xff09; 程序计数器是一块较小的内存空间&#xff0c;它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里[1]&#xff0c;字节码解释器工作时就是通过改变这个计数器的值来选取下一…

STM正点mini-跑马灯

一.库函数版 1.硬件连接 &#xff27;&#xff30;&#xff29;&#xff2f;的输出方式&#xff1a;推挽输出 &#xff29;&#xff2f;口输出为高电平时&#xff0c;P-MOS置高&#xff0c;输出为&#xff11;&#xff0c;LED对应引脚处为高电平&#xff0c;而二极管正&#…

[Tomcat] [从安装到关闭] MAC部署方式

安装Tomcat 官网下载&#xff1a;Apache Tomcat - Apache Tomcat 9 Software Downloads 配置Tomcat 1、输入cd空格&#xff0c;打开Tomca目录&#xff0c;把bin文件夹直接拖拉到终端 2、授权bin目录下的所有操作&#xff1a;终端输入[sudo chmod 755 *.sh]&#xff0c;回车 …

HCS-华为云Stack-FusionSphere

HCS-华为云Stack-FusionSphere FusionSphere是华为面向多行业客户推出的云操作系统解决方案。 FusionSphere基于开放的OpenStack架构&#xff0c;并针对企业云计算数据中心场景进行设计和优化&#xff0c;提供了强大的虚拟化功能和资源池管理能力、丰富的云基础服务组件和工具…

C++类和对象——深拷贝与浅拷贝详解

目录 1.深拷贝和浅拷贝是什么 2.案例分析 完整代码 1.深拷贝和浅拷贝是什么 看不懂没关系&#xff0c;下面有案例分析 2.案例分析 浅拷贝可能会导致堆区的内存重复释放 一个名为person的类里面有年龄和指向身高的指针这两个成员。 当我们执行到person p2&#xff08;p1&am…

翻译: GPT-4 with Vision 升级 Streamlit 应用程序的 7 种方式一

随着 OpenAI 在多模态方面的最新进展&#xff0c;想象一下将这种能力与视觉理解相结合。 现在&#xff0c;您可以在 Streamlit 应用程序中使用 GPT-4 和 Vision&#xff0c;以&#xff1a; 从草图和静态图像构建 Streamlit 应用程序。帮助你优化应用的用户体验&#xff0c;包…

深度学习-使用Labelimg数据标注

数据标注是计算机视觉和机器学习项目中至关重要的一步&#xff0c;而使用工具进行标注是提高效率的关键。本文介绍了LabelImg&#xff0c;一款常用的开源图像标注工具。用户可以在图像中方便而准确地标注目标区域&#xff0c;为训练机器学习模型提供高质量的标注数据。LabelImg…

【VB测绘程序设计】案例8——IF选择结构练习排序(附源代码)

【VB测绘程序设计】案例6——IF选择结构练习排序(附源代码) 文章目录 前言一、界面显示二、程序说明三、程序代码四、数据演示总结前言 本文主要掌握Val()函数转换,inputBox函数、IF条件句的练习,输入3个数,按大到小排序并打印。 一、界面显示 二、程序说明 利用inpu…

node.js Redis SETNX命令实现分布式锁解决超卖/定时任务重复执行问题

Redis SETNX 特性 当然&#xff0c;让我们通过一个简单的例子&#xff0c;使用 Redis CLI&#xff08;命令行界面&#xff09;来模拟获取锁和释放锁的过程。 在此示例中&#xff0c;我将使用键“lock:tcaccount_[pk]”和“status:tcaccount_[pk]”分别表示锁定键和状态键。 获…

搞定App关键词和评论

从关键词优化的三大基本概念走起&#xff01; 关联性 优化师一般如何选择关联性高的关键词呢&#xff1f; 主要思路如下&#xff1a;品牌词-关联词-竞品词-竞品关键词&#xff0c;优先级从前到后依次降低&#xff0c;通过ASO优化工具筛选出合适的关键词。做ASO有一个好处就是…

vusui css 使用,简单明了 适合后端人员 已解决

vusui-cssopen in new window 免除开发者繁复的手写 CSS 样式&#xff0c;让 WEB 前端开发更简单、灵活、便捷&#xff01;如果喜欢就点个 ★Staropen in new window 吧。 移动设备优先&#xff1a; vusui-css 包含了贯穿于整个库的移动设备优先的样式。浏览器支持&#xff1a…

vue3使用最新的属性defineModel实现父子组件数据响应式绑定

子父之间使用v-model双向绑定数据&#xff0c;子组件每次都要写emit和props觉得麻烦&#xff1f;以前&#xff0c;为了使组件支持与v-model双向绑定&#xff0c;它需要&#xff08;1&#xff09;声明prop&#xff0c;&#xff08;2&#xff09;在打算更新prop时发出相应的updat…

用C语言实现贪吃蛇游戏!!!

前言 大家好呀&#xff0c;我是Humble&#xff0c;不知不觉在CSND分享自己学过的C语言知识已经有三个多月了&#xff0c;从开始的C语言常见语法概念说到C语言的数据结构今天用C语言实现贪吃蛇已经有30余篇博客的内容&#xff0c;也希望这些内容可以帮助到各位正在阅读的小伙伴…

Linux cat,tac,more,head,tail命令 查看文本

目录 一. cat 和 tac命令二. head 和 tail 命令三. more命令 一. cat 和 tac命令 cat&#xff1a;用来打开文本文件&#xff0c;从上到下的顺序显示文件内容。tac&#xff1a;用法和cat相同&#xff0c;只不过是从下到上逆序的方式显示文件内容。当文件的内容有很多的时候&…

canvas绘制旋转的大风车

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

vue中的Mutations

目录 一&#xff1a;介绍 二&#xff1a;例子 一&#xff1a;介绍 Vuex 中的 mutation 非常类似于事件&#xff1a; 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的函数&#xff0c;并且它会接受 sta…

操作系统-线程的实现方式和多线程模型(用户级线程 内核级线程 多线程模型的情况)和线程的状态,转换,组织,控制

文章目录 线程的实现方式和多线程模型总览线程的实现方式用户级线程内核级线程多线程模型一对一多对一多对多 小结 线程的状态,转换,组织,控制总览 线程的状态与转换线程的组织与控制 线程的实现方式和多线程模型 总览 线程的实现方式 用户级线程 程序自己通过自己设计的线程…

03 Redis之命令(基本命令+Key命令+String型Value命令与应用场景)

Redis 根据命令所操作对象的不同&#xff0c;可以分为三大类&#xff1a;对 Redis 进行基础性操作的命令&#xff0c;对 Key 的操作命令&#xff0c;对 Value 的操作命令。 3.1 Redis 基本命令 一些可选项对大小写敏感, 所以应尽量将redis的所有命令大写输入 首先通过 redis-…

一行命令在 wsl-ubuntu 中使用 Docker 启动 Windows

在 wsl-ubuntu 中使用 Docker 启动 Windows 0. 背景1. 验证我的系统是否支持 KVM&#xff1f;2. 使用 Docker 启动 Windows3. 访问 Docker 启动的 Windows4. Docker Hub 地址5. Github 地址 0. 背景 我们可以在 Windows 系统使用安装 wsl-ubuntu&#xff0c;今天玩玩在 wsl-ub…

数据库查询3

目录 1. 多表查询 1.1.1 介绍 1.1.2 分类 1.2 内连接 1.3 外连接 1.4 子查询 1.4.1 介绍 1.4.2 标量子查询 1.4.3 列子查询 1.4.4 行子查询 1.4.5 表子查询 2. 事务 2.1 操作 2.2 四大特性 数据库总结2 数据库总结1 1. 多表查询 1.1.1 介绍 多表查询&#xff…