数据库实现
设计签到功能对应的数据库表
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代码
引入依赖
4.0.0com.orchidssigninbybitmap0.0.1-SNAPSHOTsigninbybitmapsigninbybitmap1.8UTF-8UTF-82.6.13org.springframework.bootspring-boot-starter-weborg.projectlomboklomboktrueorg.springframework.bootspring-boot-starter-testtestcom.github.xiaoyminknife4j-spring-boot-starter3.0.3org.springframework.bootspring-boot-starter-data-redisorg.springframework.bootspring-boot-dependencies${spring-boot.version}pomimportorg.apache.maven.pluginsmaven-compiler-plugin3.8.11.81.8UTF-8org.springframework.bootspring-boot-maven-plugin${spring-boot.version}com.orchids.signinbybitmap.SignByBitmapApplicationtruerepackagerepackage
配置文件
# 应用服务 WEB 访问端口 server: port: 8080 spring: redis: host: localhost port: 6379 password: 6379 mvc: pathmatch: matching-strategy: ant_path_matcher
knife4j配置类
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 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 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 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 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 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 SignRecords(Long userId, String key, int day) { //获取从redis中获取登录信息 List sign = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(day)).valueAt(0)); int num = sign.get(0).intValue(); LinkedList 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 { //返回码 private Integer code; //返回消息 private String message; //返回数据 private T data; public Result() { } private static Result build(T data) { Result result = new Result(); if (data != null) result.setData(data); return result; } public static Result build(T body, ResultCode resultCode) { Result result = build(body); result.setCode(resultCode.getCode()); result.setMessage(resultCode.getMessage()); return result; } public static Result ok(T data) { return build(data, ResultCode.SUCCESS); } public static Result ok() { return Result.ok(null); } public static Result fail(Integer code, String message) { Result result = build(null); result.setCode(code); result.setMessage(message); return result; } public static Result 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 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 签到内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!