天机学堂 第七天 积分系统

签到

签到最核心的包含两个要素:

  • 谁签到:用户id

  • 什么时候签的:签到日期

同时要考虑一些功能要素,比如:

  • 补签功能,所以要有补签标示

  • 按照年、月统计的功能:所以签到日期可以按照年、月、日分离保存

 

 CREATE TABLE `sign_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `year` year NOT NULL COMMENT '签到年份',
  `month` tinyint NOT NULL COMMENT '签到月份',
  `date` date NOT NULL COMMENT '签到日期',
  `is_backup` bit(1) NOT NULL COMMENT '是否补签',
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='签到记录表';

但是签到占用数据太大了

随着用户量增多、时间的推移,这张表中的数据只会越来越多,占用的空间也会越来越大。可以考虑用比特位进行签到 

实现思路 

 

像这种把每一个二进制位,与某些业务数据一一映射(本例中是与一个月的每一天映射),然后用二进制位上的数字0和1来标识业务状态的思路,称为位图。也叫做BitMap.

这种数据统计的方式非常节省空间,因此经常用来做各种数据统计。比如大名鼎鼎的布隆过滤器就是基于BitMap来实现的。

OK,那么利用BitMap我们就能直接实现签到功能,并且非常节省内存,还很高效。所以就无需通过数据库来操作了。

BitMap用法

Redis中就提供了BitMap这种结构以及一些相关的操作命令

修改某个bit位上的数据

setbit   键值 偏移量 数值(0或者1)  返回值是原来比特位上的数值,偏移量从0开始,就是选择设置第几位的值

 实例

签到(第几位上设置为1即可)

查询签到记录

 

 签到接口

而在后台,要做的事情就是把BitMap中的与签到日期对应的bit位,置为1.

另外,为了便于统计,我们计划每个月为每个用户生成一个独立的KEY,因此KEY中必须包含用户信息、月份信息,长这样:

连续签到统计

如何得到连续签到天数?需要下面几步:

  • 获取本月到今天为止的所有签到数据

  • 从今天开始,向前统计,直到遇到第一次未签到为止,计算总的签到次数,就是连续签到天数

如图:

每一次签到都做一次统计(从后向前统计),直到遇到0位置 ,每天签到都会统计一次,所以前面连续签到了8天,第九天断了,也没事,因为每次签到的时候都统计,而且第八天签到的时候奖励积分也加上了不用再加了 ,伪代码写法如下:

 

这里存在几个问题:

  • 如何才能得到本月到今天为止的所有签到记录?

  • 如何从后向前遍历每一个bit位?

至于签到记录,可以利用我们之前讲的BITFIELD命令来获取,从0开始,到今天为止的记录,命令是这样的:

遍历比特位思路: 与1做与运算,结果为1 说明签到了,这个只是与最后一位做&运算,然后按位右移(这样倒数第二位就会变成最后一位,依次类推)

  public SignResultVO addSignRecords() {
        // 1. 获取用户id
        Long userId = UserContext.getUser();
        // 2.拼接key
        LocalDate now = LocalDate.now();
        String format = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = RedisConstants.SIGN_RECORD_KEY_PREFIX + userId.toString() + format;
        // 3. 使用bitset命令 将签到记录保存到redis的bitmao结构中 需要校验是否已签到
        int offset = now.getDayOfMonth() -1;
        Boolean exists = redisTemplate.opsForValue().setBit(key, offset, true);
        // 返回值就是原来位上的值
        if (exists){
            throw  new BizIllegalException("不允许重复签到");
        }
        // 4. 计算连续签到的天数
        int signDays = countSignDays(key,now.getDayOfMonth());
        // 5.计算签到得分
        int rewardPoints = 0;
        switch (signDays) {
            case 7:
                rewardPoints = 10;
                break;
            case 14:
                rewardPoints = 20;
                break;
            case 28:
                rewardPoints = 40;
                break;
        }
        // todo 6. 保存积分
        mqHelper.send(
                MqConstants.Exchange.LEARNING_EXCHANGE,
                MqConstants.Key.SIGN_IN,
                SignInMessage.of(userId, rewardPoints + 1)); //签到积分是基本积分+奖励积分
        // 5.封装返回
        SignResultVO vo = new SignResultVO();
        vo.setSignDays(signDays);
        vo.setRewardPoints(rewardPoints);
        return vo;
    }
// 计算连续签到天数
  private int countSignDays(String key, int dayOfMonth) {
        // 从第0位取,取dayOfMonth个数,参数是可以传集合的 所以返回结果是集合,取出来的是10进制
        List<Long> result = redisTemplate.opsForValue()
                .bitField(key, BitFieldSubCommands.create().get(
                        BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
        if (CollUtils.isEmpty(result)){
            return 0;
        }
        int num = result.get(0).intValue();
        // 2.定义一个计数器
        int count = 0;
        // 从最后一位往前数连续一的个数,,每天签到一次执行一次
        while ((num & 1) == 1){
            count ++ ;
            // 数字右移一位,最后一位被舍弃,倒数第二位成了最后一位,
            num >>>= 1;
        }
        return count;
    }

需要记得语法:

localdatetime的format,datetimeformatter.ofpattern

 操作 setbit 对用的是opsforvalue的setbit方法,因为比特底层就是string类型实现的

 valueat表示从第几位开始取,取无符号位到第几位(无符号位就是整数),取出来的数是对应的从0到指定的位数对应的二进制转化的十进制

 与1做与运算会自动转化为二进制

 新增积分功能

查看该类型积分是否有每日上限,如果没有直接保存,如果有统计今日已获得的积分如果加上新积分是否会超过上限(然后保存),同时统计已经获得的总积分,放到redis中以zset方式存储

积分的获取方式多种多样,为了解耦采用MQ来实现异步解耦。 

综上,在MQ中我们只需要传递用户id一个参数即可。(因为每一种类型加的分都是固定的,可以在枚举或者常量中定义) 

发送MQ消息

在签到签到中发送的消息:填写了用户id编写消息监听器

根据不用routingkey的类型接手消息

 因为每种类型的积分可能有积分上限

首先根据雷翔判断有没有积分上限,然后数据库统计今日已经获得的积分,并累加积分到Zset

public void addPointsRecord(SignInMessage msg, PointsRecordType type) {
        // 判断该积分类型是否有上限 type.getmaxpoint()
        int maxPoints = type.getMaxPoints();
        // 涉及要增加的积分
        int realPoint = msg.getPoints();
        if (maxPoints > 0){
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime dayStartTime = DateUtils.getDayStartTime(now);
            LocalDateTime dayEndTime = DateUtils.getDayEndTime(now);
            // 如果有积分上限 查询该用户今日得到的积分
            QueryWrapper<PointsRecord> wrapper = new QueryWrapper<>();
            wrapper.select("sum(points) as totalPoints");
            wrapper.eq("user_id",msg.getUserId());
            wrapper.eq("type",type);
            wrapper.between("create_time",dayStartTime,dayEndTime);
            Map<String, Object> map = this.getMap(wrapper);
            int currebtPoints = 0;  // 当前用户 这个类型下已经获得的积分
            if (map!=null){
                BigDecimal totalPoints = (BigDecimal)map.get("totalPoints");
                currebtPoints = totalPoints.intValue();
            }
            // 3.判断已得积分是否超过上限
            if(currebtPoints >= maxPoints){
                return;
            }
            // 判断加上积分是否会超过上限
            if (currebtPoints + msg.getPoints()  > maxPoints){
                realPoint = maxPoints-currebtPoints;
            }
        }
        // 保存积分
        PointsRecord pointsRecord = new PointsRecord();
        pointsRecord.setUserId(msg.getUserId());
        pointsRecord.setType(type);
        pointsRecord.setPoints(realPoint);
        save(pointsRecord);
        // 累加并保存总积分到redis 采用zset 当前赛季排行榜
        LocalDate now = LocalDate.now();
        String format = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
        String key = RedisConstants.POINTS_BOARD_KEY_PREFIX + format;
        redisTemplate.opsForZSet().incrementScore(key,msg.getUserId().toString(),realPoint);
    }

 语法:

Map<String, Object> map = this.getMap(wrapper); 一般只有一行数据的时候采用 getmapper

 sum 聚合出来的值是bigdecimal形式

查询今日已获得的积分 

 groupby分组聚合,统计今日获得的积分

查询签到记录

在签到日历中,需要把本月第一天到今天为止的所有签到过的日期高亮显示。因此我们必须把签到记录返回,具体来说就是每一天是否签到的数据。是否签到,就是0或1,刚好在前端0和1代表false和true,也就是签到或没签到。

因此,每一天的签到结果就是一个0或1的数字,我们最终返回的结果是一个0或1组成的数组,对应从本月第1天到今天为止每一天的签到情况。

 思路分析,还是做位运算,返回值是list还是byte都行,从第0位取,取到本月的第几天-1,对每位做与运算然后放到数组中

面试

SET:点赞中用到了set,,key是业务id,内容是每个点赞的用户id

积分中用到了zset  member是每个用户ID score是积分数

注意点:

mq队列名随便写,只要routingkey匹配即可

什么时候用 getmap当结果只有一行的时候用getmap(并且使用了select()制定某个列的时候??),结果有多行用list() 

mybatisplus 使用select("sum(point) as points")  并且使用getmap接收时,结果是bigdecimal类型

十进制  & 1 会自动转化为二进制,得到的结果类型看操作数的类型(可能发生自动转化?)

redistemplate和stringredistemplate区别

  • 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。

 

StringRedisTemplate使用的是StringRedisSerializer,当你的redis数据库里面本来存的是字符串数据,或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可 

枚举

springboot整合mybatisplus通用枚举(五)---@EnumValue

 @EnumValue在mp中世纪取得的是其标注的值

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值