数据库实现
设计签到功能对应的数据库表
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='签到记录表';
这张表中的一条记录是一个用户一次的签到记录。假如一个用户1年签到100次,而网站有100万用户,就会产生1亿条记录。随着用户量增多、时间的推移,这张表中的数据只会越来越多,占用的空间也会越来越大。
redis bitmap 实现
一个用户签到的情况无非就两种,要么签了,要么没。 可以用 0 或者1如果我们按月来统计用户签到信息,签到记录为1,未签到则记录为0,就可以用一个长度为31位的二级制数来表示一个用户一个月的签到情况。最终效果如下

java代码
引入依赖
<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
xsi:schemalocation="http://maven.apache.org/pom/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelversion>4.0.0</modelversion>
<groupid>com.orchids</groupid>
<artifactid>signinbybitmap</artifactid>
<version>0.0.1-snapshot</version>
<name>signinbybitmap</name>
<description>signinbybitmap</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceencoding>utf-8</project.build.sourceencoding>
<project.reporting.outputencoding>utf-8</project.reporting.outputencoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<optional>true</optional>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-test</artifactid>
<scope>test</scope>
</dependency>
<dependency>
<groupid>com.github.xiaoymin</groupid>
<artifactid>knife4j-spring-boot-starter</artifactid>
<version>3.0.3</version>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>
</dependencies>
<dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-dependencies</artifactid>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencymanagement>
<build>
<plugins>
<plugin>
<groupid>org.apache.maven.plugins</groupid>
<artifactid>maven-compiler-plugin</artifactid>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<plugin>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-maven-plugin</artifactid>
<version>${spring-boot.version}</version>
<configuration>
<mainclass>com.orchids.signinbybitmap.signbybitmapapplication</mainclass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>配置文件
# 应用服务 web 访问端口
server:
port: 8080
spring:
redis:
host: localhost
port: 6379
password: 6379
mvc:
pathmatch:
matching-strategy: ant_path_matcherknife4j配置类
package com.orchids.signinbybitmap.web.config;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import springfox.documentation.builders.apiinfobuilder;
import springfox.documentation.builders.pathselectors;
import springfox.documentation.builders.requesthandlerselectors;
import springfox.documentation.service.apiinfo;
import springfox.documentation.service.contact;
import springfox.documentation.spi.documentationtype;
import springfox.documentation.spring.web.plugins.docket;
//import springfox.documentation.swagger2.annotations.enableswagger2;
/**
* @ author qwh
* @ date 2024/7/5 13:08
*/
@configuration
//@enableswagger2
public class knife4jconfiguration {
@bean
public docket webapiconfig(){
// 创建docket实例
docket webapi = new docket(documentationtype.swagger_2)
.groupname("studentapi")
.apiinfo(webapiinfo())
.select()
// 选择需要文档化的api,只显示指定包下的页面
.apis(requesthandlerselectors.basepackage("com.orchids.signinbybitmap"))
// 指定路径匹配规则,只对/student开头的路径进行文档化
.paths(pathselectors.regex("/user/.*"))
.build();
return webapi;
}
/**
* 构建api信息
* 本函数用于创建并返回一个apiinfo对象,该对象包含了api文档的标题、描述、版本以及联系方式等信息。
* @return 返回构建好的apiinfo对象
*/
private apiinfo webapiinfo(){
// 使用apiinfobuilder构建api信息
return new apiinfobuilder()
.title("student message api文档") // 设置文档标题
.description("本文档描述了swagger2测试接口定义") // 设置文档描述
.version("1.0") // 设置文档版本号
.contact(new contact("nullpointer", "http://blog.nullpointer.love", "nullpointer2024@gmail.com")) // 设置联系人信息
.build(); // 构建并返回apiinfo对象
}
}controller
package com.orchids.signinbybitmap.web.controller;
import com.orchids.signinbybitmap.web.domain.result.result;
import com.orchids.signinbybitmap.web.domain.vo.signresultvo;
import com.orchids.signinbybitmap.web.service.signservice;
import io.swagger.annotations.api;
import io.swagger.annotations.apioperation;
import lombok.requiredargsconstructor;
import org.springframework.web.bind.annotation.*;
/**
* @ author qwh
* @ date 2024/7/5 13:01
*/
@api(tags = "签到相关接口")
@restcontroller
@requestmapping("/user")
@requiredargsconstructor
public class signcontroller {
private final signservice signservice;
@apioperation("签到")
@getmapping("sign")
public result<signresultvo> addsignrecords() {
return signservice.addsignrecords();
}
}service
package com.orchids.signinbybitmap.web.service;
import com.orchids.signinbybitmap.web.domain.result.result;
import com.orchids.signinbybitmap.web.domain.vo.signresultvo;
/**
* @ author qwh
* @ date 2024/7/5 13:35
*/
public interface signservice {
result<signresultvo> addsignrecords();
}可以扩展其他功能
package com.orchids.signinbybitmap.web.service.impl;
import com.orchids.signinbybitmap.web.domain.result.result;
import com.orchids.signinbybitmap.web.domain.vo.signresultvo;
import com.orchids.signinbybitmap.web.exception.signexception;
import com.orchids.signinbybitmap.web.service.signservice;
import lombok.requiredargsconstructor;
import lombok.extern.slf4j.slf4j;
import org.springframework.data.redis.connection.bitfieldsubcommands;
import org.springframework.data.redis.core.stringredistemplate;
import org.springframework.stereotype.service;
import java.time.localdatetime;
import java.time.format.datetimeformatter;
import java.util.linkedlist;
import java.util.list;
/**
* @ author qwh
* @ date 2024/7/5 13:35
*/
@slf4j
@service
@requiredargsconstructor
public class signserviceimpl implements signservice {
private final string sign_uid= "sign:uid:";
private final stringredistemplate redistemplate;
@override
public result<signresultvo> addsignrecords() {
signresultvo vo = new signresultvo();
//获取签到用户
long userid = 1388888l;
//获取签到日期
localdatetime now = localdatetime.now();
string format = now.format(datetimeformatter.ofpattern(":yyyy-mm-dd"));
//设置rediskey sign:uid:1388888:2024-07-05 5 1
string key = sign_uid + userid.tostring() + format;
//计算签到偏移量
int offset = now.getdayofmonth() - 1;
//添加签到记录到redis
boolean sign = redistemplate.opsforvalue().setbit(key, offset, true);
if (sign){
throw new signexception("亲!您今天已经登录过哟 (❁´◡`❁)",520);
}
//计算连续签到天数
int day = now.getdayofmonth();
int continuedays = countsigndays(key,day);
int rewardpoints = 0;
switch (continuedays){
case 2:
rewardpoints = 10;
break;
case 4:
rewardpoints=20;
break;
case 6:
rewardpoints = 40;
break;
}
//获取签到详情信息
list<integer> signdayrecord = signrecords(userid,key,day);
vo.setuserid(userid.intvalue());
vo.setsigndays(continuedays);
vo.setrewardpoints(rewardpoints);
vo.setsignrecords(signdayrecord);
return result.ok(vo);
}
/**
* 获取连续签到天数
* @param key
* @param days
* @return
*/
private int countsigndays(string key, int days) {
//从redis读取签到记录
list<long> nums = redistemplate.opsforvalue().bitfield(key, bitfieldsubcommands.create().get(bitfieldsubcommands.bitfieldtype.unsigned(days)).valueat(0));
//计算签到次数
int num = nums.get(0).intvalue();
//num与1进行与计算得到二进制的末尾 当末尾为1 说明签到 为0 说明没有签到
int result = 0;
while ((num & 1) == 1) {
result++;
num = num >>>1;
}
//返回签到结果
return result;
}
/**
* 获取签到详情
* @param userid
* @param key
* @param day
* @return
*/
private list<integer> signrecords(long userid, string key, int day) {
//获取从redis中获取登录信息
list<long> sign = redistemplate.opsforvalue().bitfield(key, bitfieldsubcommands.create().get(bitfieldsubcommands.bitfieldtype.unsigned(day)).valueat(0));
int num = sign.get(0).intvalue();
linkedlist<integer> result = new linkedlist<>();
while (day > 0) {
result.addfirst(num & 1);
num = num >>> 1;
day--;
}
return result;
}
}其他类
package com.orchids.signinbybitmap.web.domain.result;
import lombok.data;
/**
* @ author qwh
* @ date 2024/7/5 16:52
*/
@data
public class result<t> {
//返回码
private integer code;
//返回消息
private string message;
//返回数据
private t data;
public result() {
}
private static <t> result<t> build(t data) {
result<t> result = new result<>();
if (data != null)
result.setdata(data);
return result;
}
public static <t> result<t> build(t body, resultcode resultcode) {
result<t> result = build(body);
result.setcode(resultcode.getcode());
result.setmessage(resultcode.getmessage());
return result;
}
public static <t> result<t> ok(t data) {
return build(data, resultcode.success);
}
public static <t> result<t> ok() {
return result.ok(null);
}
public static <t> result<t> fail(integer code, string message) {
result<t> result = build(null);
result.setcode(code);
result.setmessage(message);
return result;
}
public static <t> result<t> fail() {
return build(null, resultcode.fail);
}
}package com.orchids.signinbybitmap.web.domain.result;
import lombok.getter;
/**
* @ author qwh
* @ date 2024/7/5 16:54
*/
@getter
public enum resultcode {
success(200, "成功"),
fail(201, "失败"),
param_error(202, "参数不正确"),
service_error(203, "服务异常"),
data_error(204, "数据异常"),
illegal_request(205, "非法请求"),
repeat_submit(206, "重复提交"),
delete_error(207, "请先删除子集"),
admin_account_exist_error(301, "账号已存在"),
admin_captcha_code_error(302, "验证码错误"),
admin_captcha_code_expired(303, "验证码已过期"),
admin_captcha_code_not_found(304, "未输入验证码"),
admin_account_not_exist(330,"用户不存在"),
admin_login_auth(305, "未登陆"),
admin_account_not_exist_error(306, "账号不存在"),
admin_account_error(307, "用户名或密码错误"),
admin_account_disabled_error(308, "该用户已被禁用"),
admin_access_forbidden(309, "无访问权限"),
app_login_auth(501, "未登陆"),
app_login_phone_empty(502, "手机号码为空"),
app_login_code_empty(503, "验证码为空"),
app_send_sms_too_often(504, "验证法发送过于频繁"),
app_login_code_expired(505, "验证码已过期"),
app_login_code_error(506, "验证码错误"),
app_account_disabled_error(507, "该用户已被禁用"),
token_expired(601, "token过期"),
token_invalid(602, "token非法");
private final integer code;
private final string message;
resultcode(integer code, string message) {
this.code = code;
this.message = message;
}
}package com.orchids.signinbybitmap.web.domain.vo;
import com.fasterxml.jackson.annotation.jsonignore;
import io.swagger.annotations.apimodel;
import io.swagger.annotations.apimodelproperty;
import io.swagger.models.auth.in;
import lombok.data;
import java.util.list;
/**
* @ author qwh
* @ date 2024/7/5 13:36
*/
@data
@apimodel(description = "签到结果")
public class signresultvo {
@apimodelproperty("签到人")
private integer userid;
@apimodelproperty("签到得分")
private integer signpoints = 1;
@apimodelproperty("连续签到天数")
private integer signdays;
@apimodelproperty("连续签到奖励积分,连续签到超过7天以上才有奖励")
private integer rewardpoints;
@apimodelproperty("签到详细信息")
private list<integer> signrecords;
}package com.orchids.signinbybitmap.web.exception;
import com.orchids.signinbybitmap.web.domain.result.result;
import org.springframework.web.bind.annotation.controlleradvice;
import org.springframework.web.bind.annotation.exceptionhandler;
import org.springframework.web.bind.annotation.responsebody;
/**
* @ author qwh
* @ date 2024/7/5 16:51
*/
@controlleradvice
public class globalexceptionhandler {
@exceptionhandler(exception.class)
@responsebody
public result error(exception e){
e.printstacktrace();
return result.fail();
}
@exceptionhandler(signexception.class)
@responsebody
public result error(signexception e){
e.printstacktrace();
return result.fail(e.getcode(), e.getmessage());
}
}package com.orchids.signinbybitmap.web.exception;
import lombok.data;
/**
* @ author qwh
* @ date 2024/7/5 16:47
*/
@data
public class signexception extends runtimeexception{
//异常状态码
private integer code;
/**
* 通过状态码和错误消息创建异常对象
* @param message
* @param code
*/
public signexception(string message, integer code) {
super(message);
this.code = code;
}
@override
public string tostring() {
return "signexception{" +
"code=" + code +
", message=" + this.getmessage() +
'}';
}
}package com.orchids.signinbybitmap;
import org.springframework.boot.springapplication;
import org.springframework.boot.autoconfigure.springbootapplication;
@springbootapplication
public class signbybitmapapplication {
public static void main(string[] args) {
springapplication.run(signbybitmapapplication.class, args);
}
}测试结果


到此这篇关于redis bitmap 实现签到案例的文章就介绍到这了,更多相关redis bitmap 签到内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论