0
点赞
收藏
分享

微信扫一扫

BitMap实现打卡(二)

龙毓七七 2022-04-27 阅读 12

BitMap实现打卡(二)

后端环境搭建

新建项目

image.png

设置项目名

image.png

引入pom依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>linc.fun</groupId>
    <artifactId>clock-in</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.5</spring-boot.version>
        <lombok.version>1.18.24</lombok.version>
        <fastjson.version>2.0.1</fastjson.version>
        <mybatis-plus.version>3.5.1</mybatis-plus.version>
        <druid.version>1.2.9</druid.version>
        <hutool.version>5.7.22</hutool.version>
        <guava.version>31.1-jre</guava.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </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>
        <finalName>${project.name}</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>${spring-boot.version}</version>
                    <configuration>
                        <finalName>${project.build.finalName}</finalName>
                        <layers>
                            <enabled>true</enabled>
                        </layers>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

编写启动类

@SpringBootApplication
public class ClockApplication {
    public static void main(String[] args) {
        SpringApplication.run(ClockApplication.class, args);
    }
}

配置application.yml

server:
  port: 8080
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${remote-ip}:3306/clock_in?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
    username: root
    password: 123456
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: non_null
  redis:
    url: redis://123456@${remote-ip}:6379
    username: root
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        min-idle: 0
        max-idle: 8
mybatis-plus:
  mapper-locations: classpath:mapping/**/*.xml
  global-config:
    db-config:
      id-type: AUTO
      banner: false
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    jdbc-type-for-null: 'null'

remote-ip: localhost

编写公共返回结果集

public abstract class DTO implements Serializable {
    private static final long serialVersionUID = 1L;
}

@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
@NoArgsConstructor
public class Response extends DTO {
    private static final long serialVersionUID = 1L;
    private boolean success;
    private String errCode;
    private String errMessage;


    public static Response buildFailure(String errCode, String errMessage) {
        Response response = new Response();
        response.setSuccess(false);
        response.setErrCode(errCode);
        response.setErrMessage(errMessage);
        return response;
    }

    public static Response buildSuccess() {
        Response response = new Response();
        response.setSuccess(true);
        return response;
    }

    @Override
    public String toString() {
        return "Response [isSuccess=" + this.success + ", errCode=" + this.errCode + ", errMessage=" + this.errMessage + "]";
    }
}
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class SingleResponse<T> extends Response {
    private T data;


    public static <T> SingleResponse<T> of(T data) {
        SingleResponse<T> singleResponse = new SingleResponse<>();
        singleResponse.setSuccess(true);
        singleResponse.setData(data);
        return singleResponse;
    }


    public static SingleResponse<?> buildFailure(String errCode, String errMessage) {
        SingleResponse<?> response = new SingleResponse<>();
        response.setSuccess(false);
        response.setErrCode(errCode);
        response.setErrMessage(errMessage);
        return response;
    }

    public static SingleResponse<?> buildSuccess() {
        SingleResponse<?> response = new SingleResponse<>();
        response.setSuccess(true);
        return response;
    }
}

编写返回集统一处理

定义相关注解,便于管理

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@RestController
public @interface ResponseResult {
    /**
     * 判断是否可以忽略
     */
    boolean ignore() default false;
}

重写ResponseBodyAdvice

@Slf4j
@ControllerAdvice
@AllArgsConstructor
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {


    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        final var method = methodParameter.getMethod();
        final var clazz = Objects.requireNonNull(method, "method is null").getDeclaringClass();

        // 判断类上面是否加了@ResponseResult
        var annotation = clazz.getAnnotation(ResponseResult.class);

        if (Objects.isNull(annotation)) {
            annotation = method.getAnnotation(ResponseResult.class);
        }

        // 如果是FileSystemResource 则不拦截
        if (method.getAnnotatedReturnType().getType().getTypeName().equals(FileSystemResource.class.getTypeName())) {
            return false;
        }
        return annotation != null && !annotation.ignore();

    }


    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object data, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        var singleResponse = SingleResponse.of(data);

        if ((data instanceof String) && !MediaType.APPLICATION_XML_VALUE.equals(mediaType.toString())) {
            ObjectMapper om = new ObjectMapper();
            return om.writeValueAsString(singleResponse);
        }

        if (Objects.isNull(data) && MediaType.TEXT_HTML_VALUE.equals(mediaType.toString())) {
            ObjectMapper om = new ObjectMapper();
            return om.writeValueAsString(singleResponse);
        }

        return singleResponse;
    }
}

配置全局配置Bean

配置MyBatis Plus

@Configuration
@MapperScan("linc.fun.*.mapper")
@EnableTransactionManagement
public class ApplicationConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

编写测试接口

@ResponseResult
@RequestMapping("/api")
public class TestController {

    @GetMapping("/test")
    public Map<String, Object> test() {
        Map<String, Object> map = new HashMap<>();
        map.put("info", "你懂的");
        System.out.println("11111");
        return map;
    }
}

数据库创建

创建数据库

CREATE DATABASE clock_in charset utf8mb4;
USE clock_in;
CREATE TABLE `user` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
	`name` varchar(30) NOT NULL COMMENT '姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户';

编写相关实体

public class PO implements Serializable {
    @TableField(exist = false)
    private static final long serialVersionUID = 1534419234554796749L;
}
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "`user`")
public class User extends PO {
    /**
     * ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 姓名
     */
    @TableField(value = "`name`")
    private String name;
}

编写MyBatis相关映射

public interface UserMapper extends BaseMapper<User> {
}
public interface UserService extends IService<User>{
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="linc.fun.clock.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="linc.fun.clock.po.User">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
    </resultMap>
</mapper>

实现后端打卡接口

创建控制层

先约定好要写拿几个接口

@ResponseResult
@RequestMapping("/api")
public class UserController {
    @Resource
    private UserService service;

    /**
     * 用户当天打卡
     */
    @PostMapping("/todayClockIn/{id}")
    public void todayClockIn(@PathVariable Long id) {
        service.todayClockIn(id);
    }

    /**
     * 判断当天是否打卡
     */
    @GetMapping("/checkTodayClockIn/{id}")
    public boolean checkTodayClockIn(@PathVariable Long id) {
        return service.checkTodayClockIn(id);
    }

    /**
     * 获取当月用户打卡信息
     */
    @GetMapping("/getClockInCurrentMonthInfo/{id}")
    public UserClockInfoVO getClockInCurrentMonthInfo(@PathVariable Long id) {
        return service.getClockInCurrentMonthInfo(id);
    }

    /**
     * 判断当前用户当月连续签到次数
     */
    @GetMapping("/getContinuousClockInTimes/{id}")
    public int getContinuousClockInTimes(@PathVariable Long id) {
        return service.getContinuousClockInTimes(id);
    }
}
@EqualsAndHashCode(callSuper = true)
@Data
public class UserClockInfoVO extends VO {
    /**
     * 打卡列表
     */
    List<ClockIn> clockInfo;


    /**
     * 打卡
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ClockIn {
        /**
         * 日期
         * 2022-04-06
         */
        private String day;
        /**
         * 是否打卡
         */
        private Boolean clock;
    }
}
public class VO implements Serializable {

    private static final long serialVersionUID = 9106194241611838335L;
}

定义Redis常量

public interface RedisKeyConstants {
    /**
     * 用户打卡key
     */
    String USER_SIGN_IN_KEY = "USER_SIGN_IN_KEY";

    /**
     * 后缀
     */
    String SUFFIX = "_";



    /**
     * 生成: SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY_9_10
     *
     * @param redisKey SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY
     * @param args     9,10
     *
     * @return SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY_9_10
     */
    static String generateKeyConstantWithSuffix(String redisKey, String... args) {
        if (args.length == 0) {
            return redisKey + RedisKeyConstants.SUFFIX;
        } else {
            StringBuilder sb = new StringBuilder(redisKey);
            for (String arg : args) {
                sb.append(RedisKeyConstants.SUFFIX).append(arg);
            }
            return sb.toString();
        }
    }

    /**
     * 如 SIMBOT_RANDOM_ALGORITHM_QUESTION_KEY_
     * 判断是否以这个key为前缀
     *
     * @param text        需要校验的字符串
     * @param keyConstant 常量
     *
     * @return true|false
     */
    static boolean startsWithKeySuffix(String text, String keyConstant) {
        return text.startsWith(keyConstant + SUFFIX);
    }

    /**
     * 分割如 SIMBOT_SENSITIVE_ACCOUNT_KEY_1220127046
     *
     * @param text        SIMBOT_SENSITIVE_ACCOUNT_KEY_1220127046
     * @param keyConstant SIMBOT_SENSITIVE_ACCOUNT_KEY_
     *
     * @return [1220127046]
     */
    static String[] splitKeyConstantParam(String text, String keyConstant) {
        if (startsWithKeySuffix(text, keyConstant)) {
            return text.substring((keyConstant + SUFFIX).length()).split(SUFFIX);
        }
        return null;
    }
}

定义打卡Key

对于两个场景

  • 用户月签到

    • 月签到key: USER_SIGN_IN_KEY_<id>_2022-4

      比如说id为1的用户在2022年4月签到信息

       bit: 0   1   0  1  0  1
    offset: 0   1   2  3  4  5     偏移量表示这个月的第几天
    含义:       签到   签到   签到
    
  • 月签到用户数

    • 签到用户key: USER_SIGN_IN_KEY_2022-4

      比如说2022年4月有多少人签到了,全部用这个key存放

         bit: 0   1   0  1  0  1
      offset: 0   1   2  3  4  5     便宜量表示这个月签到用户的id
      含义:       签到   签到   签到
      

自定义BitMapUtil

@Component
public class BitMapUtil {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 设置key字段第offset位bit数值
     *
     * @param key    字段
     * @param offset 位置
     * @param value  数值
     */
    public void setBit(String key, long offset, boolean value) {
        stringRedisTemplate.execute((RedisCallback<?>) con -> con.setBit(key.getBytes(), offset, value));
    }

    /**
     * 判断该key字段offset位否为1
     *
     * @param key    字段
     * @param offset 位置
     *
     * @return 结果
     */
    public boolean getBit(String key, long offset) {
        return (boolean) stringRedisTemplate.execute((RedisCallback<?>) con -> con.getBit(key.getBytes(), offset));

    }

    /**
     * 统计key字段value为1的总数
     *
     * @param key 字段
     *
     * @return 总数
     */
    public Long bitCount(String key) {
        return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
    }

    /**
     * 统计key字段value为1的总数,从start开始到end结束
     *
     * @param key   字段
     * @param start 起始
     * @param end   结束
     *
     * @return 总数
     */
    public Long bitCount(String key, Long start, Long end) {
        return (Long) stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitCount(key.getBytes(), start, end));
    }

    /**
     * 取多个key并集并计算总数
     *
     * @param key key
     *
     * @return 总数
     */
    public Long opOrCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.OR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return this.bitCount(key[0] + "To" + key[key.length - 1]);
    }

    /**
     * 取多个key的交集并计算总数
     *
     * @param key key
     *
     * @return 总数
     */
    public Long opAndCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.AND, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return this.bitCount(key[0] + "To" + key[key.length - 1]);
    }

    /**
     * 取多个key的补集并计算总数
     *
     * @param key key
     *
     * @return 总数
     */
    public Long opXorCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.XOR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return this.bitCount(key[0] + "To" + key[key.length - 1]);
    }

    /**
     * 取多个key的否集并计算总数
     *
     * @param key key
     *
     * @return 总数
     */
    public Long opNotCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        stringRedisTemplate.execute((RedisCallback<?>) con -> con.bitOp(RedisStringCommands.BitOperation.NOT, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        stringRedisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return this.bitCount(key[0] + "To" + key[key.length - 1]);
    }

    /**
     * 从第start位开始取offset位结果为无符号数的
     * 有符号数最多可以获取64位, 无符号数只能获取63位
     *
     * @param key    key
     * @param start  从第几位开始
     * @param offset 偏移量多少
     *
     * @return 取出的值
     */
    public List<Long> bitfield(String key, int start, int offset) {
        return stringRedisTemplate.execute((RedisCallback<List<Long>>) con -> con.bitField(key.getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(start)).valueAt(offset)));
    }
}

实现用户当天打卡功能

 private SimpleDateFormat getYearAndMonthSdf() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");
        sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
        return sdf;
    }
 public void todayClockIn(Long id) {
        // 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
        // 签到用户key: USER_SIGN_IN_KEY_2022-4
        /**
         * 完成月签到
         */
        SimpleDateFormat sdf = getYearAndMonthSdf();
        // 获取当天日期 2022-04-27
        LocalDate nowDate = LocalDate.now();
        // 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
        String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
        // 获取今天是这个月的第几天
        int dayOfMonth = nowDate.getDayOfMonth();
        // 设置偏移量
        bitMapUtil.setBit(monthClockInKey, dayOfMonth, true);

        /**
         * 完成签到用户
         */
        // 签到用户key: USER_SIGN_IN_KEY_2022-4
        String clockInUserKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, sdf.format(Date.valueOf(nowDate)));
        // 设置偏移量
        bitMapUtil.setBit(clockInUserKey, id, true);

    }

实现判断当天是否打卡

public boolean checkTodayClockIn(Long id) {
        SimpleDateFormat sdf = getYearAndMonthSdf();
        // 获取当天日期 2022-04-27
        LocalDate nowDate = LocalDate.now();
        // 获取今天是这个月的第几天
        int dayOfMonth = nowDate.getDayOfMonth();
        // 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
        String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
        return bitMapUtil.getBit(monthClockInKey, dayOfMonth);
    }

实现获取当月用户打卡信息

 public UserClockInfoVO getClockInCurrentMonthInfo(Long id) {
        SimpleDateFormat sdf = getYearAndMonthSdf();
        // 获取当天日期 2022-04-27
        LocalDate nowDate = LocalDate.now();
        // 获取今天是这个月的第几天
        int dayOfMonth = nowDate.getDayOfMonth();
        // 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
        String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
        // 设置这个月有多天打卡了
        UserClockInfoVO vo = new UserClockInfoVO();
        vo.setClockInfo(Lists.newArrayListWithCapacity(dayOfMonth));
        // 这个月有多少天
        int lengthOfMonth = nowDate.lengthOfMonth();
        // 从第一位开始因为我设置的id默认从1开始
        List<Long> bitfields = bitMapUtil.bitfield(monthClockInKey, lengthOfMonth, 1);
        if (!CollectionUtils.isEmpty(bitfields)) {
            // 由低位->高位,为0表示未签到,为1表示已签到
            long v = Objects.isNull(bitfields.get(0)) ? 0 : bitfields.get(0);
            for (int i = lengthOfMonth; i > 0; i--) {
                // 过去的日期
                LocalDate pastDate = nowDate.withDayOfMonth(i);
                UserClockInfoVO.ClockIn clockIn = new UserClockInfoVO.ClockIn();
                // 设置日期 2022-4-27
                clockIn.setDay(String.valueOf(pastDate));
                // 设置是否打卡
                clockIn.setClock(v >> 1 << 1 != v);
                // 右移一位
                v >>= 1;
                // 添加到集合
                vo.getClockInfo().add(clockIn);
            }
        }
        return vo;
    }

实现判断当前用户当月连续签到次数

 public int getContinuousClockInTimes(Long id) {
        SimpleDateFormat sdf = getYearAndMonthSdf();
        // 获取当天日期 2022-04-27
        LocalDate nowDate = LocalDate.now();
        // 月签到key: USER_SIGN_IN_KEY_<id>_2022-4
        String monthClockInKey = RedisKeyConstants.generateKeyConstantWithSuffix(RedisKeyConstants.USER_SIGN_IN_KEY, String.valueOf(id), sdf.format(Date.valueOf(nowDate)));
        int clockInTimes = 0;
        // 从第一位开始因为我设置的id默认从1开始
        List<Long> bitfields = bitMapUtil.bitfield(monthClockInKey, nowDate.getDayOfMonth(), 1);
        if (!CollectionUtils.isEmpty(bitfields)) {
            // 由低位连续不为0的个数即为连续签到次数,需要考虑没有签到的情况
            long v = Objects.isNull(bitfields.get(0)) ? 0 : bitfields.get(0);
            for (int i = 0; i < nowDate.getDayOfMonth(); i++) {
                // 先右移再左移,如果相同就代表低位为0
                if (v >> 1 << 1 == v) {
                    if (i > 0) break;
                } else {
                    clockInTimes++;
                }
                v >>= 1;
            }
        }
        return clockInTimes;
    }

实现前端打卡接口

编写前端接口api/clock.ts

import request from "../util/request";

export const test = () => {
    return request({
        url: '/test',
        method: 'GET'
    })
}

/**
 * 用户当天打卡
 */
export const todayClockIn = (id: number): any => {
    return request({
        url: `/todayClockIn/${id}`,
        method: 'POST'
    })
}
/**
 * 判断当天是否打卡
 */
export const checkTodayClockIn = (id: number): any => {
    return request({
        url: `/checkTodayClockIn/${id}`,
        method: 'GET'
    })
}

/**
 * 获取当月用户打卡信息
 */
export const getClockInCurrentMonthInfo = (id: number): any => {
    return request({
        url: `/getClockInCurrentMonthInfo/${id}`,
        method: 'GET'
    })
}


/**
 * 判断当前用户当月连续签到次数
 */
export const getContinuousClockInTimes = (id: number): any => {
    return request({
        url: `/getContinuousClockInTimes/${id}`,
        method: 'GET'
    })
}

编写页面请求

我们假定用户是id为1的,因为这里我们没有做登录等功能,所以暂定用id位1的用户

<script setup lang="ts">
import {onMounted, ref, unref} from "vue"
import {ElNotification} from "element-plus"
import {todayClockIn, checkTodayClockIn, getClockInCurrentMonthInfo, getContinuousClockInTimes} from '../api/clock'

const userId = 2;

interface ClockData {
  day: string,
  clock: boolean
}

interface UserClockInfoVO {
  clockInfo: Array<any>
}


// 是否已经打卡
let isClocked = ref<boolean>(false)
// 打卡信息
let clockData = ref<Array<ClockData>>([])
// 连续签到次数
let clockTimes = ref<Number>(0)

// 打卡
const clockIn = async () => {
  await todayClockIn(userId)
  ElNotification({showClose: true, message: '打卡成功', type: 'success', duration: 2000})
  isClocked.value = true
  // 重新获取
  await getClockInfo()
}
// 判断是否打卡
const checkClockIn = async () => {
  isClocked.value = await checkTodayClockIn(userId)
}
// 获取打卡信息
const getClockInfo = async () => {
  let data: UserClockInfoVO = await getClockInCurrentMonthInfo(userId);
  clockData.value = data.clockInfo
}
// 获取连续签到次数
const getContinuousClock = async () => {
  clockTimes.value = await getContinuousClockInTimes(userId)
}
onMounted(() => {
  checkClockIn()
  getClockInfo()
  getContinuousClock()
})

</script>

连续签到标签添加

 <el-badge :value="clockTimes" type="primary">
          <el-button>连续签到</el-button>
 </el-badge>

前端最终代码

<script setup lang="ts">
import {onMounted, ref, unref} from "vue"
import {ElNotification} from "element-plus"
import {todayClockIn, checkTodayClockIn, getClockInCurrentMonthInfo, getContinuousClockInTimes} from '../api/clock'

const userId = 2;

interface ClockData {
  day: string,
  clock: boolean
}

interface UserClockInfoVO {
  clockInfo: Array<any>
}


// 是否已经打卡
let isClocked = ref<boolean>(false)
// 打卡信息
let clockData = ref<Array<ClockData>>([])
// 连续签到次数
let clockTimes = ref<Number>(0)

// 打卡
const clockIn = async () => {
  await todayClockIn(userId)
  ElNotification({showClose: true, message: '打卡成功', type: 'success', duration: 2000})
  isClocked.value = true
  // 重新获取
  await getClockInfo()
}
// 判断是否打卡
const checkClockIn = async () => {
  isClocked.value = await checkTodayClockIn(userId)
}
// 获取打卡信息
const getClockInfo = async () => {
  let data: UserClockInfoVO = await getClockInCurrentMonthInfo(userId);
  clockData.value = data.clockInfo
}
// 获取连续签到次数
const getContinuousClock = async () => {
  clockTimes.value = await getContinuousClockInTimes(userId)
}
onMounted(() => {
  checkClockIn()
  getClockInfo()
  getContinuousClock()
})

</script>

<template>
  <div class="picker">
    <el-card style="height: 760px">
      <div style="display: flex;justify-content: space-between">
        <span style="font-size: 22px;display: block;margin-bottom: 30px;margin-left: 10px;">打卡</span>
        <el-badge :value="clockTimes" type="primary">
          <el-button>连续签到</el-button>
        </el-badge>
        <span v-if="!isClocked"><el-button type="primary" class="el-icon-s-promotion" style="border: none"
                                           @click="clockIn"><span
            style="color: white;font-weight: bolder">未打卡</span></el-button></span>
        <span v-else><el-button type="success" class="el-icon-s-promotion" style="border: none"><span
            style="color: white;font-weight: bolder">已打卡</span></el-button></span>
      </div>
      <el-calendar :first-day-of-week="7">
        <template #dateCell="{ data }">
          <p>{{ data.day.split('-').slice(2).join('-') }}<br/></p>
          <div v-for="(item, index) in clockData" :key="index">
            <div v-if="data.day === item.day">
                  <span v-if="item.clock">
                 <i class="el-icon-check" style="color: green;font-weight: bolder;">已打卡</i>
                  </span>
            </div>
          </div>
        </template>
      </el-calendar>
    </el-card>
  </div>

</template>

<style scoped lang="scss">
.picker {
  display: flex;
  justify-content: center;
  align-items: center;
}

::v-deep(.el-calendar-table .el-calendar-day) {
  padding: 0;
}

::v-deep(.el-calendar-table td.is-today ) {
  background-color: #fff;
}

::v-deep(.el-calendar-table td.is-selected ) {
  background-color: #fff;
}

::v-deep(.el-calendar-table .el-calendar-day) {
  height: 60px;
  font-size: 12px;
  text-align: center;
}

::v-deep(.el-calendar-table:not(.is-range) td.prev) {
  .calendarFont {
    color: #C0C4CC;
  }

  pointer-events: none;
}

::v-deep(.el-calendar-table thead th) {
  font-size: 12px;
  padding-bottom: 6px;
}

.cal ::v-deep(.el-calendar-day .calendar_circle1) {
  margin: 0 auto;
  padding: 2px;
  text-align: center;
}

.cal ::v-deep(.el-calendar-day .calendar_circle2) {
  border: 1px solid #DE4747;
  border-radius: 50%;
  margin: 0 auto;
  padding: 2px;
  text-align: center;
}
</style>

最终实现效果

image.png

代码获取

举报

相关推荐

0 条评论