1.简介
BitMap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 Byte,所以bitmap 本身会极大的节省储存空间。 bitmap最大为2的32次方个bit
setbit
- SETBIT key offset value
- key :设置的别名
- offset:字节偏移量(二进制偏移量)
- value:值(0和1),二进制中只有0和1两个数据
getbit
-
GETBIT key offset
-
key :设置的别名
-
offset:字节偏移量(二进制偏移量)
bitcount
- BITCOUNT key [start end]
- key :设置的别名
- start,end 二进制位的offset 开始和结束的角标位
- 返回在这些角标位中二进制为1的数量,如果key为时间,可以统计出这一天中有多少个用户在活跃
BITOP
-
BITOP operation destkey key [key …]
-
operation : AND 、 OR 、 NOT 、 XOR
-
destkey : 合并目标key
-
key : 20200901
例:BITOP and destkey 20200901 20200902
合并两天的活跃用户数
2.使用场景
用户在线状态
统计活跃用户
用户签到
…
/**
* 签到页面数据
* @return mixed|void
*/
public function checkPage(){
try {
$uid = \request()->get('uid');
return $this->sign->checkPage($uid);
}catch (\Exception $exception){
return error('请明天再签');
}
}
/**
* 签到
* @return array|false|string
*/
public function sign()
{
//开启事物
DB::beginTransaction();
try {
$uid = \request()->get('uid');
if (!is_numeric($uid) || $uid !=(int)$uid || $uid <= 0){
return error('uid参数错误',);
}
$year = date('Ym');
$num = request()->get('num');
$date = date('Ymd');
if ($date > $year.$num){
DB::commit();
return $this->sign->retroactive($uid,$year.$num);
}elseif ($date < $year.$num){
return error('时间未到');
}
//提交
DB::commit();
return $this->sign->Sign($uid);
}catch (\Exception $exception){
//回滚
DB::rollBack();
return error($exception->getMessage());
}
}
<?php
namespace App\Repository\Repositories;
use App\Models\IntegralRecord;
use App\Models\User;
use App\Models\UserSign;
use App\Repository\RepositoryInterface\SignInterface;
use Illuminate\Support\Facades\Redis;
class SignRepositories implements SignInterface
{
/**
* 签到页面数据
* @param $uid
* @return mixed|void
*/
public function checkPage($uid)
{
$month = date('Ym');
$day = date('d');
$i = 0;
$signKey = $uid . ':' . $month;
$sign = Redis::getbit($signKey, $day);
$user = (new User())->where('uid',$uid)->first()->toarray();
if ($sign == 1) {
//连续签到次数
$min = self::getNum($uid, $month, $day,$i);
return success('查询成功',['num' => $min,'day' => date('d'),'integral' => $user['integral'],'user' => $user,'year' => date('Y'),'month' => date('m')]);
}
return success('查询成功',['num' => $i,'day' => date('d'),'integral' => $user['integral'],'user' => $user,'year' => date('Y'),'month' => date('m')]);
}
/**
* 签到
* @param $uid
* @return false|string
*/
public function Sign($uid)
{
//获取当前时间
$now = date('Ym');
$day = date('d'); //04
//拼接bitmap的key
$key = $uid.':'.$now; //"1:202207"
//查看是否签到过
$date = Redis::getbit($key,$day);
if($date) return success('今天已签到');
//获取昨天时间
$lastDay = date("d", strtotime("-1 day")); //"02"
//查看昨天是否签到
$lastDate = Redis::getbit($key,$lastDay);
$user = User::where('uid',$uid)->first();
//昨天签到(连签)
if($lastDate){
//正常所得积分30 额外所得积分50 一共80积分
if ($user['sign_num'] == 5 || $user['sign_num'] == 11 || $user['sign_num'] == 17){
$getIntegral = 80;
}elseif($user['sign_num'] >= 3){
$getIntegral = 30;
}else{
$getIntegral = 10;
}
$num = $user['sign_num'] + 1;
$integralFen = $user['integral'] + $getIntegral;
$result = User::where('uid',$uid)->update(['sign_num' => $num,'integral' => $integralFen]);
if (!$result) return error('签到失败');
$signRes = UserSign::insert(['uid' => $uid,'title' => "用户第$num 次签到",'number' => $getIntegral,'balance' => $integralFen,'add_time' => time()]);
if (!$signRes) return error('签到失败');
IntegralRecord::insert(['uid' => $uid,'integral_source' => '2','integral_increase' => $getIntegral,'add_time' => time()]);
Redis::setbit($key,$day,1);
return success("您已连续签到$num 天,已得积分$integralFen 分",['num' => $num,'day' => date('d'),'integral' => $integralFen]);
}
//昨天未签到(断签)
$getIntegral = 10;
$num = 1;
$result = User::where('uid',$uid)->update(['sign_num' => $num,'integral' => $user['integral'] + $getIntegral]);
if (!$result) return error('签到失败');
$signRes = UserSign::insert(['uid' => $uid,'title' => "用户$uid 签到",'number' => $getIntegral,'balance' => $user['integral'] + $getIntegral,'add_time' => time()]);
if (!$signRes) return error('签到失败');
IntegralRecord::insert(['uid' => $uid,'integral_source' => 2,'integral_increase' => $getIntegral,'add_time' => time()]);
Redis::setbit($key,$day,1);
return success('签到成功',['num' => $num,'day' => date('d'),'integral' => $user['integral'] + $getIntegral]);
}
/**
* 补签
* @param $uid
* @param $date
* @return false|mixed|string
*/
public function retroactive($uid, $date)
{
//获取今天的时间
$now = date('Ymd');
if ($date >= $now) return error('补签的日期不能大于等于今天');
//获取要补签的时间
$day = substr($date, 6, 2);
$month = substr($date, 0, 6);
$key = $uid . ':' . $month;
$sign = Redis::getbit($key, $day); //0
if ($sign == 1) return error('你已经签到过了不需要补签');
Redis::setbit($key, $day, 1);
//查询用户信息
$user = User::where('uid',$uid)->first()->toarray();
//每次补签需花费30积分
$fire = 30;
if($user['integral'] < $fire) return error('您的积分不足,不符合补签的条件');
$integral = $user['integral'] - $fire;
$month = date('Ym');
$day = date('d') - 1;
$signKey = $uid . ':' . $month;
$sign = Redis::getbit($signKey, $day);
$i = 1;
if ($sign == 1) {
//连续签到次数
$i = self::getNum($uid, $month, $day,$i);
User::where('uid',$uid)->update(['integral' => $integral,'sign_num' => $i]);
}
$signRes = UserSign::insert(['uid' => $uid,'title' => "用户$uid 补签",'number' => 0,'balance' => $integral,'add_time' => time()]);
if (!$signRes) return error('签到失败');
IntegralRecord::insert(['uid' => $uid,'integral_end' => 2,'integral_reduce' => $fire,'add_time' => time()]);
return success("您补签成功",['num' => $i,'day' => date('d'),'integral' => $integral]);
}
/**
* 计算连续签到的天数
* @param $uid
* @param $date
* @param $day
* @param $i
* @return false|int|mixed|string
*/
public static function getNum($uid, $date, $day,&$i)
{
try {
$key = $uid . ':' . $date;
$sign = 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);
}
}
return $i;
}catch (\Exception $e){
return error($e->getMessage());
}
}
}
wxml
<!--index.wxml-->
<calendar defaultDate="{{toYear}}/{{toMonth}}/{{dateString}}" spot="{{list}}" bind:dateChange="dateChange"></calendar>
<view class="date-string">当前选中的日期是:{{date}}</view>
<view class='sign-com'>
<view class='thead'>
<view class='tt'>已连续签到</view>
<view class='mm'><label class='n'>{{signNum}}</label>天</view>
<view class='pp'>你的积分为{{ score}}</view>
</view>
</view>
<view class="doc-box">
<view class="title">属性</view>
<view class="prpos">defaultDate</view>
<view class="explain">日历默认选中的时间</view>
<view class="type">String | Date | Number(可以被dayjs解析的格式即可)</view>
<view class="default">今天</view>
<view class="prpos">spot</view>
<view class="explain">底部需要展示小圆点的日期数组</view>
<view class="type">Array{{'<'}}String | Date | Number{{'>'}}</view>
<view class="default">[]</view>
<view class="title">事件</view>
<view class="event">bind:dateChange</view>
<view class="explain">选中的日期变化时触发</view>
<view class="callback">event.detail:{ date, month, year, dateString }</view>
<view class="event">bind:monthChange</view>
<view class="explain">选中的月份变化时触发</view>
<view class="callback">event.detail:{ date, month, year, dateString }</view>
<view class="event">bind:yearChange</view>
<view class="explain">选中的年份变化时触发</view>
<view class="callback">event.detail:{ date, month, year, dateString }</view>
</view>
js
//index.js
Page({
/**
* 页面的初始数据
*/
data: {
signNum:0,//签到天数
toMonth: parseInt(new Date().getMonth() + 1), //本月
toYear: parseInt(new Date().getFullYear()), //本年
dateString: parseInt(new Date().getDate()),
// spot: ['2022/7/6', '2022/7/9'],
list:[]
},
dateChange(e) {
let that = this
let num = e.detail.date
console.log(num)
if(num > this.data.dateString){
wx.showToast({
title: '时间未到',
icon:'none'
})
return false
}
let ckl = this.data.toYear+'/'+this.data.toMonth+'/'+num
let list = this.data.list
list.push(ckl)
// console.log(ckl)
// console.log(list)
this.setData({
date:num,
list:list
})
let uid = 1
wx.request({
url: 'http://www.ckl.com/api/sign', //仅为示例,并非真实的接口地址
data: {
num,
uid
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
that.setData({
signNum:res.data.data.num,
score:res.data.data.integral
})
}
})
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
let that = this
let uid = 1
wx.request({
url: 'http://www.ckl.com/api/checkPage', //仅为示例,并非真实的接口地址
data: {
uid
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
let year = res.data.data.year
let month = res.data.data.month
let num = res.data.data.num
let day = res.data.data.day
for(let i=num;i>0;i--){
let ckl = year+'/'+month+'/'+day
day--
let list = that.data.list
list.push(ckl)
that.setData({
list:list
})
}
that.setData({
signNum:res.data.data.num,
score:res.data.data.integral
})
}
})
},
})
wxss
/**index.wxss**/
page {
background-color: #f6f6f6;
}
.date-string {
margin-top: 20rpx;
background-color: #fff;
font-size: 28rpx;
padding: 0 30rpx;
line-height: 60rpx;
}
.sign-com {
width: 100%;
height: auto;
padding: 0 30rpx;
box-sizing: border-box;
overflow: hidden;
}
.sign-com .thead {
width: 100%;
text-align: center;
padding: 50rpx 0 35rpx;
}
.sign-com .thead .tt {
font-size: 24rpx;
}
.sign-com .thead .mm {
margin-top: 10rpx;
font-size: 24rpx;
}
.sign-com .thead .mm .n {
font-size: 66rpx;
margin-right: 25rpx;
}
.sign-com .thead .pp {
color: #999;
font-size: 24rpx;
margin-top: 10rpx;
}
第二种
wxml
<view class='signIn'>
<view class='explax'>
<view class=''><image src="{{avatar}}" style="border-radius: 60%;width: 60px;height: 60px;float: left"></image></view>
<view class='' style="font-size: 18px;font-weight: 800;float: left;margin-left: 10px;">{{nickname}}</view>
</view>
<view class='sign-com'>
<view class='thead'>
<view class='tt'>已连续签到</view>
<view class='mm'><label class='n'>{{signNum}}</label>天</view>
<view class='pp'>你的积分为{{ score}}</view>
</view>
<view class='modle'>
<view class='mol'>
<view class='mol-line'></view>
<view class='mol-ites'>
<view class="ite {{signNum>=4?'hover':''}}" data-n='{{min-3}}'>
<label class='n'>+{{signNum<7?10:30}}</label>
</view>
<view class="ite {{signNum>=3?'hover':''}}" data-n='{{min-2}}'>
<label class='n'>+{{signNum<7?10:30}}</label>
</view>
<view class="ite {{signNum>=2?'hover':''}}" data-n='{{min-1}}'>
<label class='n'>+{{signNum<4?10:30}}</label>
</view>
<view class="ite {{signNum>=1?'hover':''}}" data-n='{{min}}'>
<label class='n'>+{{signNum<3?10:30}}</label>
</view>
<view class="ite {{signNum>=min+4?'hover':''}}" data-n='{{min+1}}'>
<label class='n'>+{{signNum+1<3?10:30}}</label>
</view>
<view class="ite {{signNum+2>7?'hover':''}}" data-n='{{min+2}}'>
<label class='n'>+{{signNum+2<3?10:30}}</label>
</view>
<view class="ite {{signNum+2>7?'hover':''}}" data-n='{{min+3}}'>
<label class='n'>+{{signNum+2<3?10:30}}</label>
</view>
</view>
</view>
<view class='moday'>
<label class='dd' bindtap='bindSignIn' data-num="{{min-3}}">{{min-3}}号</label>
<label class='dd' bindtap='bindSignIn' data-num="{{min-2}}">{{min-2}}号</label>
<label class='dd' bindtap='bindSignIn' data-num="{{min-1}}">{{min-1}}号</label>
<label class='dd' bindtap='bindSignIn' data-num="{{min}}">{{min}}号</label>
<label class='dd' bindtap='bindSignIn' data-num="{{min+1}}">{{min+1}}号</label>
<label class='dd' bindtap='bindSignIn' data-num="{{min+2}}">{{min+2}}号</label>
<label class='dd' bindtap='bindSignIn' data-num="{{min+4}}">{{min+3}}号</label>
</view>
</view>
</view>
</view>
<view class='explax'>
<view class=''>签到数:{{signNum}}天</view>
<view class=''>您的积分为:{{score}}</view>
</view>
js
// pages/sign/sign.js
Page({
/**
* 页面的初始数据
*/
data: {
days: [],
SignUp: [],
cur_year: 0, //当前选的年
cur_month: 0, //当前选的月
min: parseInt(new Date().getDate()), //本日
toMonth: parseInt(new Date().getMonth() + 1), //本月
toYear: parseInt(new Date().getFullYear()), //本年
weeks_ch: ['日', '一', '二', '三', '四', '五', '六'],
weeks_en: ['Sun', 'Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat'],
},
bindSignIn(c){
console.log(c)
let that = this
let num = c.currentTarget.dataset.num
let uid = 1
wx.request({
url: 'http://www.ckl.com/api/sign', //仅为示例,并非真实的接口地址
data: {
num,
uid
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
that.setData({
signNum:res.data.data.num,
score:res.data.data.integral
})
}
})
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
let that = this
let uid = 1
wx.request({
url: 'http://www.ckl.com/api/checkPage', //仅为示例,并非真实的接口地址
data: {
uid
},
header: {
'content-type': 'application/json' // 默认值
},
success (res) {
console.log(res.data)
that.setData({
signNum:res.data.data.num,
score:res.data.data.integral,
nickname:res.data.data.user.nickname,
avatar:res.data.data.user.avatar
})
}
})
},
})
wxss
.signIn {
width: 100%;
height: 220px;
}
.sign-com {
width: 100%;
height: auto;
padding: 0 30rpx;
box-sizing: border-box;
overflow: hidden;
}
.sign-com .thead {
width: 100%;
text-align: center;
padding: 50rpx 0 35rpx;
}
.sign-com .thead .tt {
font-size: 24rpx;
}
.sign-com .thead .mm {
margin-top: 10rpx;
font-size: 24rpx;
}
.sign-com .thead .mm .n {
font-size: 66rpx;
margin-right: 25rpx;
}
.sign-com .thead .pp {
color: #999;
font-size: 24rpx;
margin-top: 10rpx;
}
.sign-com .modle {
width: 100%;
height: 100rpx;
margin-top: 10rpx;
}
.sign-com .modle .mol {
width: 100%;
height: 52rpx;
position: relative;
}
.sign-com .mol-line {
width: 100%;
height: 4rpx;
background-color: #e6e6e6;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
.sign-com .mol-ites {
width: 100%;
height: 100%;
position: absolute;
}
.mol-ites .ite {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
border: 1px solid #f5f5f5;
background-color: #fff;
box-sizing: border-box;
position: absolute;
left: 0;
top: 0;
z-index: 2;
}
.mol-ites .ite .n {
width: 44rpx;
height: 44rpx;
line-height: 44rpx;
text-align: center;
border-radius: 50%;
background-color: #f5f5f5;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 22rpx;
}
.mol-ites .ite::after {
content: "";
width: 80rpx;
height: 4rpx;
background-color: transparent;
position: absolute;
left: 52rpx;
top: 50%;
margin-top: -2rpx;
z-index: 2;
}
.mol-ites .ite:last-of-type::after {
width: 0;
}
.mol-ites .ite:nth-of-type(2) {
left: 107rpx;
}
.mol-ites .ite:nth-of-type(3) {
left: 214rpx;
}
.mol-ites .ite:nth-of-type(4) {
left: 321rpx;
}
.mol-ites .ite:nth-of-type(5) {
left: 428rpx;
}
.mol-ites .ite:nth-of-type(6) {
left: 535rpx;
}
.mol-ites .ite:nth-of-type(7) {
left: 642rpx;
}
.mol-ites .ite.hover {
border-color: #ff614a;
}
.mol-ites .ite.hover .n {
background-color: #ff614a;
color: #fff;
}
.mol-ites .ite.hover::after {
background-color: #ff614a;
}
.moday {
width: 100%;
height: 80rpx;
overflow: hidden;
position: relative;
margin-top: 20rpx;
}
.moday .dd {
width: 52rpx;
height: 80rpx;
line-height: 1;
text-align: center;
font-size: 22rpx;
position: absolute;
left: 0;
bottom: 0;
}
.moday .dd:nth-of-type(2) {
left: 107rpx;
}
.moday .dd:nth-of-type(3) {
left: 214rpx;
}
.moday .dd:nth-of-type(4) {
left: 321rpx;
}
.moday .dd:nth-of-type(5) {
left: 428rpx;
}
.moday .dd:nth-of-type(6) {
left: 535rpx;
}
.moday .dd:nth-of-type(7) {
left: 642rpx;
}
.the-btn {
margin: 50rpx 0;
}
.the-btn .btn {
background-color: #ff614a;
color: #fff;
}
.the-btn.signed .btn {
background-color: rgba(153, 153, 153, 0.61);
}
.explax {
margin-top: 40px;
padding: 0 30rpx;
font-size: 28rpx;
color: #666;
}