需求:
1)签到1天得1积分,连续签到2天得2积分,3天得3积分,3天以上均得3积分
2)如果连续签到中断,则重置计数,并且每月重置计数
3)显示用户某月的签到次数
4)在日历控件上展示用户每月签到,可以切换年月显示
对于用户签到数据,如果直接采用数据库存储,当出现高并发访问时,对数据库压力会很大。所以采用缓存来减轻数据库的压力
Redis提供的数据类型BitMap(位图),每个bit对应0/1状态,BitMap的内部是采用String存储,但是提供了一些指令对BitMap直接操作
常见指令:
实现方式:
用户每月存储一条签到数据。key的格式为u:sign:{uid}:{yyyMM} ,而value采用长度为4个字节(32bit)的BitMap,BitMap的每一位代表每一天的签到,1表示已签,0表示未签
例如:
SETBIT u:sign:1225:202403 5 1 #代表ID为1225的用户在2024年3月的签到记录 5 1 表示3月6号签到(因为偏移量是从0开始,所以把6减1)
GETBIT u:sign:1225:202403 5 #检查用户在3月6号的签到结果
BITCOUNT u:sign:1225:202403 #统计2024年3月的签到次数
BITPOS u:sign:1225:202403 1 #返回3月首次签到的日期
BITFIELD u:sign:1225:202403 get u31 0 #获取3月份前31天得签到数据
实例代码:
import java.util.Date;
import java.util.Map;
public interface UserSignService {
//用户签到
boolean doSign(int uid,Date date);
//检查用户是否签到
boolean checkSign(int uid,Date date);
//获取用户签到次数
long getSignCount(int uid,Date date);
//获取当月连续签到次数
long GetContinuousSignCount(int uid,Date date);
//获取当月首次签到日期
Date getFirstSignDate(int uid,Date date);
//获取本月签到情况
Map<String,Boolean> getSignInfo(int uid,Date date);
}
package com.yxy.pet.service.impl;
import com.sun.tools.javac.util.List;
import com.yxy.pet.service.UserSignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.redis.connection.BitFieldSubCommands;
@Service
public class UserSignServiceImpl implements UserSignService {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public boolean doSign(int uid, Date date) {
int offset=date.getDay()-1;
String key=buildSignKey(uid,date);
return redisTemplate.opsForValue().setBit(key,offset,true);
}
@Override
public boolean checkSign(int uid, Date date) {
int offset=date.getDay()-1;
String key=buildSignKey(uid,date);
return redisTemplate.opsForValue().getBit(key,offset);
}
@Override
public long getSignCount(int uid, Date date) {
String key=buildSignKey(uid,date);
Long bitCount = (Long) redisTemplate.execute((RedisCallback<Long>) connection -> {
return connection.bitCount(key.getBytes());
});
return bitCount != null ? bitCount : 0;
}
private String buildSignKey(int uid, Date date) {
return "u:sign:"+uid+":"+ FormatDate(date,"yyyyMM");
}
private static String FormatDate(Date date, String pattern) {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
return sdf.format(date);
}
@Override
public long GetContinuousSignCount(int uid, Date date) {
String key=buildSignKey(uid,date);
int signCount = 0;
String type = "u" + date.getDay(); // 取1号到当天的签到状态
//通过redisTemplate执行bitField命令,获取存储在redis位图中制定偏移量的值
Object result = redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitField(key.getBytes(), BitFieldSubCommands.get(BitFieldSubCommands.BitFieldType.unsigned(date.getDay())))
);
//通过位运算检查每一天的签到状态,计算连续签到的天数
if (result != null) {
long[] list = (long[]) result;
if (list.length > 0) {
long v = list[0];
for (int i = 0; i < date.getDay(); i++) {
if ((v >> 1) << 1 == v) {
if (i > 0) break;
} else {
signCount += 1;
}
v >>= 1;
}
}
}
return signCount;
}
@Override
public Date getFirstSignDate(int uid, Date date) {
String key=buildSignKey(uid,date);
Long pos = redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitPos(key.getBytes(), true));
return pos < 0 ? null : new Date(date.getTime() - (date.getDay() - pos + 1) * 24 * 60 * 60 * 1000);
}
@Override
public Map<String, Boolean> getSignInfo(int uid, Date date) {
String key=buildSignKey(uid,date);
Map<String, Boolean> signMap = new HashMap<>();
String type = "u" + getDayOfMonth(date);
Object result = redisTemplate.execute((RedisCallback<Object>) connection ->
connection.bitField(key.getBytes(), BitFieldSubCommands.get().unsigned(getDayOfMonth(date)))
);
if (result != null) {
long[] list = (long[]) result;
if (list.length > 0) {
long v = list[0];
for (int i = getDayOfMonth(date); i > 0; i--) {
Date d = new Date(date.getTime() + (i - date.getDay()) * 24 * 60 * 60 * 1000);
signMap.put(FormatDate(d, "yyyy-MM-dd"), ((v >> 1) << 1) != v);
v >>= 1;
}
}
}
return signMap;
}
private static int getDayOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int month = calendar.get(Calendar.MONTH) + 1;
if (month == 2) {
return 28;
}
if (List.of(1, 3, 5, 7, 8, 10, 12).contains(month)) {
return 31;
}
return 30;
}
}