签到业务流程

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.mfbz.cn/a/352249.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

git:使用git rebase合并多次commit为一个

git log&#xff1a;找到需要合并的最早 commit 的父级 git rebase -i 73a5cd8597除第一个 pick 外&#xff0c;将其它改成 s&#xff0c;改完后保存退出 保存完后弹出 commit message 合并提示&#xff0c;根据这次合并的目的&#xff0c;重写commit message&#xff0c;改完后…

计算机中丢失mfc100u.dll怎么解决,详细解析mfc100u.dll丢失的解决方法

遭遇“无法找到mfc100u.dll”的错误不必过分担忧&#xff0c;这是一个普遍现象。许多用户在启动某些软件或游戏的时候可能会碰到这样的情况。通常&#xff0c;这个错误信息表明你的计算机系统中缺失了一个关键的动态链接库(DLL)文件&#xff0c;它可能会妨碍应用程序的顺利启动…

Android读写文件,适配Q以上

Android Q升级了文件系统&#xff0c;访问文件不仅仅是说动态权限了&#xff0c;有各种限制。权限什么的就不赘述了&#xff0c;下面介绍一下在10以上的系统中访问文件。 首先是打开文件管理器 /*** 打开文件管理器 存储卡和外接U盘都可以访问*/public void openFileManager()…

没指定spring-boot-maven-plugin版本导致编译失败,这坑你踩过没

文章目录 1. 前言2. 组件版本信息3. 事件经过3.1 本地通过maven命令打包3.2 定位问题步骤3.2.1 核对spring-boot-maven-plugin版本信息3.2.2 spring-boot-maven-plugin版本错误原因 3.3 解决方案 4.结论 1. 前言 我们在平时开发过程中关注的比较多的是项目开发时依赖包的版本以…

为什么选择快速应用开发:提高业务响应速度与竞争力的关键

如今&#xff0c;企业想要持续蓬勃发展&#xff0c;就需要具备快速满足客户期望的能力。无论是十几年历史的重要市场占有者推出新的APP&#xff0c;还是在疫情期间从线下转向线上电商营销&#xff0c;企业都需要主动适应市场。随着为客户提供新的服务方式&#xff0c;员工也需要…

【LUA】mac状态栏添加天气

基于网络上的版本修改的&#xff0c;找不到出处了。第一个摸索的lua脚本&#xff0c;调了很久。 主要修改&#xff1a;如果风速不大&#xff0c;就默认不显示&#xff0c;以及调整为了一些格式 local urlApi http://.. --这个urlApi去申请个免费的就可以了 然后打开对应的json…

Scratch:启蒙少儿编程的图形化魔法

在当今这个数字化时代&#xff0c;编程已经成为了一项重要的基础技能。就像学习阅读和写作一样&#xff0c;掌握编程能够打开通往未来世界的大门。对于孩子们来说&#xff0c;Scratch作为一种图形化编程语言&#xff0c;不仅简单有趣&#xff0c;而且非常适合作为编程学习的入门…

Vue3 Suspense 优雅地处理异步组件加载

✨ 专栏介绍 在当今Web开发领域中&#xff0c;构建交互性强、可复用且易于维护的用户界面是至关重要的。而Vue.js作为一款现代化且流行的JavaScript框架&#xff0c;正是为了满足这些需求而诞生。它采用了MVVM架构模式&#xff0c;并通过数据驱动和组件化的方式&#xff0c;使…

基于51单片机智能电子秤

实物显示效果&#xff1a; https://www.bilibili.com/video/BV1Wb4y1A7Aw/?vd_source6ff7cd03af95cd504b60511ef9373a1d 功能介绍&#xff1a; &#xff08;1&#xff09;用键盘设计单价&#xff1b; &#xff08;2&#xff09;称重后同时显示该物品的重量、单价和总额&…

JAVA编程题之用户登录,用户信息存储在本地文件

实现用户登录&#xff1a;键盘输入要登录的用户名与密码 properties类型文件常在框架内用作配置文件. public static void main(String[] args) throws Exception {FileInputStream fis new FileInputStream("user.properties");Properties properties new Prope…

Hive3.1.3基础(续)

参考B站尚硅谷 分区表和分桶表 分区表 Hive中的分区就是把一张大表的数据按照业务需要分散的存储到多个目录&#xff0c;每个目录就称为该表的一个分区。在查询时通过where子句中的表达式选择查询所需要的分区&#xff0c;这样的查询效率会提高很多。 分区表基本语法 分区表…

一、对人工智能大模型了解与认知

黑8说 月黑风高&#xff0c;乌云密布&#xff0c;树木低垂&#xff0c;黯淡沉闷。这黎明前的风暴&#xff0c;预示着新时代的变革即将到来。 在一个8线小城市的办公室中 黑8对主任说&#xff1a; 世界上有男人、女人、人妖&#xff0c;米国有1/3男&#xff0c;2/3女…&#xff…

HCIP实验7-三层架构实验

搭建实验拓扑图 实验开始 配置r1,r2的IP地址及环回 r1 [r1]interface LoopBack 0 [r1-LoopBack0]ip address 1.1.1.1 32 [r1]interface g0/0/0 [r1-GigabitEthernet0/0/0]ip address 23.1.1.1 24 [r1]interface g0/0/1 [r1-GigabitEthernet0/0/1]ip address 34.1.1.1 24 [r1…

Bitbucket第一次代码仓库创建/提交/创建新分支/合并分支/忽略ignore

1. 首先要在bitbucket上创建一个项目&#xff0c;这个我没有权限创建&#xff0c;是找的管理员创建的。 管理员创建之后&#xff0c;这个项目给了我权限&#xff0c;我就可以创建我的代码仓库了。 2. 点击这个Projects下的具体项目名字&#xff0c;就会进入这样一个页面&#…

11k+ star 一款不错的笔记leanote安装教程

特点 支持普通模式 支持markdown模式 支持搜索 安装教程 1.安装mongodb 1.1.下载 #下载 cd /opt wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.0.1.tgz 1.2解压 tar -xvf mongodb-linux-x86_64-3.0.1.tgz 1.3配置mongodb环境变量 vim /etc/profile 增…

java集合ArrayList和HashSet的fail-fast与fail-safe以及ConcurrentModificationException

在 java 的集合工具类中&#xff0c;例如对 ArrayList 或者 HashSet 进行删除元素后再遍历元素时&#xff0c;会抛出 ConcurrentModificationException 异常。 fail-fast ArrayList public class TestList {public static void main(String[] args) {ArrayList<Integer>…

Java 基于SpringBoot+Vue的母婴商城系统,附源码,文档

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

宝塔面板一键部署幻兽帕鲁联机服务器教程

幻兽帕鲁是一款深受玩家喜爱的多人在线游戏&#xff0c;玩家可以自行搭建联机服务器&#xff0c;邀请亲朋好友一起在世界探索畅玩&#xff0c;为了方便玩家更快速的部署自己的幻兽帕鲁联机服务器&#xff0c;宝塔面板推出了幻兽帕鲁一键部署服务&#xff0c;一键即可安装并调整…

2024.1.24 GNSS 学习笔记

1.伪距观测值公式 2.载波相位观测值公式 3.单点定位技术(Single Point Positionin, SPP) 仅使用伪距观测值&#xff0c;不使用其他的辅助信息获得ECEF框架下绝对定位技术。 使用广播星历的轨钟进行定位&#xff0c;考虑到轨钟的米级精度&#xff0c;所以对于<1米的误差&…

【STM32】STM32学习笔记-W25Q64简介(37)

00. 目录 文章目录 00. 目录01. SPI简介02. W25Q64简介03. 硬件电路04. W25Q64框图05. Flash操作注意事项06. 预留07. 附录 01. SPI简介 在大容量产品和互联型产品上&#xff0c;SPI接口可以配置为支持SPI协议或者支持I 2 S音频协议。SPI接口默认工作在SPI方式&#xff0c;可以…