PHP实现RedisBitMap签到功能(补签/签到送积分)

391 阅读4分钟

大家好,我是正文

本章简单跟大家讲一下PHP使用redisBitmap做一个签到功能

首先,我们要理解什么是Bitmap类型 理解了以后才方便理解下面的内容

首先是reids存储bitmap的方式

setbit key offset val

gitbit key offset

常用的两种命令

可以看得出,他是以key + 偏移量 + 值进行存储的

我们可以用二进制去理解他 如 0000010010011111111 这样的

一般这样的一串是一个key所代表的内容 而里面的下标 也就不言而喻的 每一个单位值

bitmap存储的值都是使用1/0来进行储存

所以 套入到签到功能 当前的用户:年月 作为key 而 day 可以作为offset

直接在PHP进行获取并使用即可

签到

1、判断当前日是否被签到 
2、判断前一天我们是否有签到过
3、查询mysql中是否存在签到记录

若没有签到记录 则说明是首次签到 
setbit签到后
我们要先使用模型添加一条记录 同时初始化连签次数和积分赠送
这个时候 首次签到就已经完成了

而连签的逻辑在于 
1、不是首次签到
2、签到前一天有成功签到的记录
我们可以正常跑逻辑了
首先前一天已经签到了 一般来讲积分赠送都是递归或者根据需求进行积分的发放
所以我们可以使用数据库中的连签次数*给定的积分 + 每日独立的签到积分奖励
然后将连签次数+1
添加完成后 可以进行数据库积分的发放 或者是 奖励方法
如积分奖励的计算 或是 优惠劵发放等

若不是连签 也就是断签进行签到
我们可以直接进行签到的定义 然后将数据库中你的连签次数转为1 积分奖励依然是一样的 
<?php

namespace App\Http\Controllers\api;

use App\Busincess\Common\CommonBusincess;
use App\Busincess\Common\Json;
use App\Busincess\Logic\SignBusincess;
use App\Http\Controllers\Controller;
use App\Models\Coupon;
use App\Models\Sign;

class SignController extends Controller
{
    protected static $redis;

    public function __construct()
    {
        self::$redis = new \Redis();
        self::$redis->connect('127.0.0.1', 6379);
    }

    /**
     * Created by PhpStorm
     * Name:signIn
     * User: 立早正文
     * DateTime: 2022/11/13 13:14
     * Desc:发起签到
     */
    public function actionUserSignIn()
    {
        $uid = \request()->uid;
        //查看今天的签到状态
        $date = date('Y-m', time());
        $day = date('d', time());
        $key = $uid . ":" . $date;
        //获取指定bitmap列 中的下标 得到当前下标到状态
        $nowSignStatus = self::$redis->getBit($key, $day);
        if ($nowSignStatus) return Json::error(config('code.Error'), '您已经签到过了');
        //查看昨天的签到状态
        $yesterday = date('d', strtotime('-1 day'));
        $yesterdaySignStatus = self::$redis->getBit($key, $yesterday);
        $signInfo = Sign::findUid($uid);
        //第一次签到
        if (empty($signInfo)) {
            $params = [
                'uid' => $uid,
                'sign_num' => 1,
                'is_backup' => 0,
                'integral' => 10
            ];
            $result = Sign::create($params);
            self::$redis->setBit($key, $day, 1);
            return Json::succeed(config('bingo'), '恭喜您首次签到成功');
        }
        //不是首次签到 判断昨日是否签到
        if ($yesterdaySignStatus) {
            //连签
            $signInfo['integral'] += 10 + ($signInfo['sign_num'] * 10);
            $signInfo['sign_num'] += 1;
            $signInfo->save();
            if ($signInfo['sign_num'] == 7) {
                //7连签
                $signInfo['integral'] += 50;
                $signInfo->save();
                return Json::succeed(config('bingo'), '恭喜您连续签到成功,已向您赠送50积分');
            }
            if ($signInfo['sign_num'] == 14) {
                //14连签
                $params = [
                    'uid' => $uid,
                    'name' => "满50 - 5 优惠劵",
                    'min_price' => 50,
                    'minus_price' => 5
                ];
                Coupon::create($params);
                return Json::succeed(config('bingo'), '恭喜您连续签到14天,已向您的账户发放了满50 - 5 优惠劵');
            }
            if ($signInfo['sign_num'] == 30) {
                //月连签
                $params = [
                    'uid' => $uid,
                    'name' => "无门槛5000000元优惠劵",
                    'min_price' => 0,
                    'minus_price' => 5
                ];
                Coupon::create($params);
                return Json::succeed(config('bingo'), '恭喜您连续签到31天,已向您的账户发放了无门槛5000000元优惠劵');
            }
            self::$redis->setBit($key, $day, 1);
            return Json::succeed(config('bingo'), '恭喜您连续签到成功');
        } else {
            //断签
            $signInfo['sign_num'] = 1;
            $signInfo['integral'] += 10;
            $signInfo->save();
            self::$redis->setBit($key, $day, 1);
            return Json::succeed(config('bingo'), '恭喜您重新签到成功');
        }


    }


    /**
     * Created by PhpStorm
     * Name:getMonthSign
     * User: 立早正文
     * DateTime: 2022/11/13 17:35
     * Desc:当月签到情况
     */
    public function getMonthSign()
    {
        try {

            $uid = \request()->uid;
            $date = date('Y-m', time());
            //获取用户签到信息
            $key = $uid . ':' . $date;
            $sign = self::$redis->get($key);
            //处理数据
            $bitmap_bin_str = CommonBusincess::strToBin($sign);
            return Json::succeed(config('code.bingo'), '本月签到详情', $bitmap_bin_str);
        } catch (\Exception $e) {
            return Json::error(config('code.Error'), $e->getMessage());
        }
    }

```
首先获取到用户要进行补签的日期
判断补签是佛合法 比如重复补签 、日期是否正常
判断后正常进行补签
记得扣除指定积分后再签到
有三个时间比较重要
补签日 前一天 当前日期




```
/**
     * Created by PhpStorm
     * Name:actionRepair
     * User: 立早正文
     * DateTime: 2022/11/13 16:47
     * Desc:补签
     */
    public function actionRepair()
    {
        try {
            //uid
            $uid = \request()->uid;
            //补签具体的哪一天
            $day = \request()->post('day');
            //当前月
            $date = date('Y-m', time());
            //比较补签时间与当前时间
            if (strtotime($date . '-' . $day) >= strtotime(date('Y-m-d', time()))) return Json::error(config('code.Error'), '补签异常');
            //补签当月的bit $key
            $key = $uid . ":" . $date;
            //今天
            $thisDay = date('d', time());
            //获取补签天的签到状态
            $signStatus = self::$redis->getBit($key, $day);
            if ($signStatus) return Json::error(config('code.Error'), '该日已签到');
            //消耗30积分补签
            $signInfo = Sign::findUid($uid);
            $signInfo->integral -= 30;
            $signInfo->save();
            //发起补签
            self::$redis->setBit($key, $day, 1);
            $lastDay = date('d',strtotime('-1 day'));
            $lastDaySatatus = self::$redis->getBit($key,$lastDay);
            $i = 1 ;
            if($lastDaySatatus)
            {
                //查询前一天的用户连签次数
                SignBusincess::getNum($uid,$date,$lastDay,$i);
                //查询今日签到状态
                $thisDaySignStatus = self::$redis->getBit($key,$thisDay);
                //若签到
                if($thisDaySignStatus)
                {
                    //补签前的连签次数+1
                    $signInfo = Sign::findUid($uid);
                    $signInfo->sign_num = $i + 1;
                    $signInfo->save();
                    return Json::succeed(config('code.bingo'),'补签成功,恭喜您现在连续补签'.$signInfo->sign_num."天");
                }
                //若今日没签 载入补签后的连签次数
                $signInfo = Sign::findUid($uid);
                $signInfo->sign_num = $i;
                $signInfo->save();
                return Json::succeed(config('code.bingo'),'补签成功,恭喜您现在连续补签'.$signInfo->sign_num."天");
            }
            //
            Sign::findUid($uid);
            $signInfo->sign_num = 1;
            $signInfo->save();
            return Json::succeed(config('code.bingo'),'补签成功,恭喜您现在连续补签'.$signInfo->sign_num."天");
        } catch (\Exception $e) {
            return Json::error(config('code.Error'), $e->getMessage());
        }

    }


}


/**
 * 获取用户连续签到天数
 * @param $uid
 * @param $date
 * @param $day
 * @param $i
 */
public static function getNum($uid, $date, $day, &$i)
{
    try {
        $key = $uid . ':' . $date;
        $sign = self::$redis->getBit($key, $day);
        if ($sign == 1) {
            $i++;
            if ($day - 1 > 0) {
                self::getNum($uid, $date, $day - 1, $i);
            } else {
                $time = strtotime($date . '0' . $day) - 86400;
                $mytime = date('Ymd', $time);
                $day = substr($mytime, 6, 2);
                $month = substr($mytime, 0, 6);
                self::getNum($uid, $month, $day, $i);
            }
        }
    } catch (\Exception $e) {
        return Json::error(500, $e->getMessage());
    }
}

/**
 * 数据转换为数字
 * @param $str
 * @return string
 */
public static function strToBin($str)
{
    // 对数据流使用网络字节序(大端)解包拿到16进制数据的字符串形式
    $hex_str = unpack("H*", $str)[1];
    // hex str 的长度
    $hex_str_len = strlen($hex_str);
    // 为了防止 hex to dec 时发生溢出
    // 我们需要切分 hex str,使得每一份 hex str to dec 时都能落在 int 类型的范围内
    // 因为 2 位 16 进制表示一个字节,所以用系统 int 类型的字节长度去分组是绝对安全的
    $chunk_size = PHP_INT_SIZE;
    // 对 hex str 做分组对齐,否则 str 的最后几位可能会被当作低位数据处理
    // 比如 fffff 以 4 位拆分 'ffff', 'f' 后 最后一组 'f' 就被低位数据处理了
    // 对齐后 fffff000 分组 'ffff', 'f000' 就能保证 'f' 的数据位了
    $hex_str = str_pad($hex_str, $hex_str_len + ($chunk_size - ($hex_str_len % $chunk_size)), 0, STR_PAD_RIGHT);
    // 防止 hexdec 时溢出 使用 PHP_INT_SIZE 个 16 进制字符一组做拆分
    // 因 16 进制 2 位标识一个字节 所以 PHP_INT_SIZE 是绝对不会溢出的
    $hex_str_arr = str_split($hex_str, $chunk_size);
    // 位数据的二进制字符串
    $bitmap_bin_str = '';
    array_walk($hex_str_arr, function ($hex_str_chunk) use (&$bitmap_bin_str, $chunk_size) {
        $bitmap_bin_str .= str_pad(decbin(hexdec($hex_str_chunk)), $chunk_size * 4, 0, STR_PAD_LEFT);
    });
    return $bitmap_bin_str;
}

image.png