数据库实现
设计签到功能对应的数据库表
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_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<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);
}
}