数据库实现
设计签到功能对应的数据库表
1 2 3 4 5 6 7 8 9
| 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代码
引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <?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>
|
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13
|
server: port: 8080
spring: redis: host: localhost port: 6379 password: 6379 mvc: pathmatch: matching-strategy: ant_path_matcher
|
knife4j配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| 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;
@Configuration
public class knife4jConfiguration { @Bean public Docket webApiConfig(){ Docket webApi = new Docket(DocumentationType.SWAGGER_2) .groupName("StudentApi") .apiInfo(webApiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.orchids.signinbybitmap")) .paths(PathSelectors.regex("/User/.*")) .build(); return webApi; }
private ApiInfo webApiInfo(){ return new ApiInfoBuilder() .title("Student message API文档") .description("本文档描述了Swagger2测试接口定义") .version("1.0") .contact(new Contact("nullpointer", "http://blog.nullpointer.love", "nullpointer2024@gmail.com")) .build(); } }
|
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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.*;
@Api(tags = "签到相关接口") @RestController @RequestMapping("/User") @RequiredArgsConstructor public class SignController { private final SignService signService; @ApiOperation("签到") @GetMapping("Sign") public Result<SignResultVO> AddSignRecords() { return signService.AddSignRecords(); } }
|
service
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.orchids.signinbybitmap.web.service;
import com.orchids.signinbybitmap.web.domain.result.Result; import com.orchids.signinbybitmap.web.domain.vo.SignResultVO;
public interface SignService { Result<SignResultVO> AddSignRecords(); }
|
可以扩展其他功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| 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;
@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")); String key = SIGN_UID + userId.toString() + format; int offset = now.getDayOfMonth() - 1; 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); }
private int countSignDays(String key, int days) {
List<Long> nums = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(days)).valueAt(0)); int num = nums.get(0).intValue(); int result = 0; while ((num & 1) == 1) { result++; num = num >>>1; } return result; }
private List<Integer> SignRecords(Long userId, String key, int day) {
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; } }
|
其他类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.orchids.signinbybitmap.web.domain.result;
import lombok.Data;
@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); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| package com.orchids.signinbybitmap.web.domain.result;
import lombok.Getter;
@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; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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;
@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;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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;
@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()); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package com.orchids.signinbybitmap.web.exception;
import lombok.Data;
@Data public class SignException extends RuntimeException{ private Integer code;
public SignException(String message, Integer code) { super(message); this.code = code; }
@Override public String toString() { return "SignException{" + "code=" + code + ", message=" + this.getMessage() + '}'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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); }
}
|
测试结果

